Repository: dj-stripe/dj-stripe

Issue:
Cancelling a Subscription removes the model instance
I notice at the moment when you cancel a subscription the model instance is deleted too and looking at the code this seems to be by design. Is this the correct action to take - surely having the history would be useful and would there not be foreign keys to invoices etc? What purpose is the subscription status of "cancelled" if the records are deleted? I also noticed that the Customer._sync_subscriptions() method pulls in all subscriptions regardless of their status so the functionality at least seems to be inconsistent.


Discussion:
It seems Customer._sync_invoices() will import the canceled subscriptions too
@jleclanche @lskillen thoughts? Keeping cancelled subscriptions would be ideal, but I remember there being some reason for this deletion
I'd be willing to bet the reason for it is Stripe's old behaviour with listing cancelled subscriptions. I had to make some updates to that when I updated dj-stripe for the 2017- apis. Either way, yeah, we should be keeping them. 
Any updates on this?
Closing as dupe of #576 since it's the same underlying issue: We're handling the .deleted webhooks wrong.
I worked around this problem in my fork as follows:

in event_handlers.py:

```
@webhooks.handler("customer.subscription")
def customer_subscription_webhook_handler(event):
	"""Handle updates to customer subscription objects.
	Docs an example subscription webhook response: https://stripe.com/docs/api#subscription_object
	"""
	# XXX workaround: don't delete canceled subscriptions
	crud_type = CrudType.determine(event=event)
	if crud_type.deleted:
		crud_type = CrudType(updated=True)
	_handle_crud_like_event(target_cls=Subscription, event=event, crud_type=crud_type)
```


Resolving patch:
diff --git a/djstripe/event_handlers.py b/djstripe/event_handlers.py
--- a/djstripe/event_handlers.py
+++ b/djstripe/event_handlers.py
@@ -111,7 +111,16 @@ def customer_subscription_webhook_handler(event):
     Docs an example subscription webhook response:
     https://stripe.com/docs/api#subscription_object
     """
-    _handle_crud_like_event(target_cls=models.Subscription, event=event)
+
+    # customer.subscription.deleted doesn't actually delete the subscription
+    # on the stripe side, it updates it to canceled status, so override
+    # crud_type to update to match.
+    crud_type = CrudType.determine(event=event)
+    if crud_type.deleted:
+        crud_type = CrudType(updated=True)
+    _handle_crud_like_event(
+        target_cls=models.Subscription, event=event, crud_type=crud_type
+    )
 
 
 @webhooks.handler("payment_method")


Test patch:
diff --git a/tests/test_event_handlers.py b/tests/test_event_handlers.py
--- a/tests/test_event_handlers.py
+++ b/tests/test_event_handlers.py
@@ -9,6 +9,7 @@
 from django.test import TestCase
 from stripe.error import InvalidRequestError
 
+from djstripe.enums import SubscriptionStatus
 from djstripe.models import (
     Card,
     Charge,
@@ -74,6 +75,7 @@
     FAKE_PLAN,
     FAKE_PRODUCT,
     FAKE_SUBSCRIPTION,
+    FAKE_SUBSCRIPTION_CANCELED,
     FAKE_SUBSCRIPTION_III,
     FAKE_TRANSFER,
     IS_STATICMETHOD_AUTOSPEC_SUPPORTED,
@@ -353,13 +355,18 @@ def test_customer_subscription_deleted(
         event = self._create_event(FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED)
         event.invoke_webhook_handlers()
 
-        Subscription.objects.get(id=FAKE_SUBSCRIPTION["id"])
+        sub = Subscription.objects.get(id=FAKE_SUBSCRIPTION["id"])
+        self.assertEqual(sub.status, SubscriptionStatus.active)
+
+        subscription_retrieve_mock.return_value = deepcopy(FAKE_SUBSCRIPTION_CANCELED)
 
         event = self._create_event(FAKE_EVENT_CUSTOMER_SUBSCRIPTION_DELETED)
         event.invoke_webhook_handlers()
 
-        with self.assertRaises(Subscription.DoesNotExist):
-            Subscription.objects.get(id=FAKE_SUBSCRIPTION["id"])
+        sub = Subscription.objects.get(id=FAKE_SUBSCRIPTION["id"])
+        # Check that Subscription is canceled and not deleted
+        self.assertEqual(sub.status, SubscriptionStatus.canceled)
+        self.assertIsNotNone(sub.canceled_at)
 
     @patch("stripe.Customer.retrieve", autospec=True)
     @patch("stripe.Event.retrieve", autospec=True)