We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `djstripe/event_handlers.py`
Content:
```
1 """
2 Webhook event handlers for the various models
3 
4 Stripe docs for Events: https://stripe.com/docs/api/events
5 Stripe docs for Webhooks: https://stripe.com/docs/webhooks
6 
7 TODO: Implement webhook event handlers for all the models that need to
8       respond to webhook events.
9 
10 NOTE:
11     Event data is not guaranteed to be in the correct API version format.
12     See #116. When writing a webhook handler, make sure to first
13     re-retrieve the object you wish to process.
14 
15 """
16 import logging
17 
18 from . import models, webhooks
19 from .enums import SourceType
20 from .utils import convert_tstamp
21 
22 logger = logging.getLogger(__name__)
23 
24 
25 @webhooks.handler("customer")
26 def customer_webhook_handler(event):
27     """Handle updates to customer objects.
28 
29     First determines the crud_type and then handles the event if a customer
30     exists locally.
31     As customers are tied to local users, djstripe will not create customers that
32     do not already exist locally.
33 
34     Docs and an example customer webhook response:
35     https://stripe.com/docs/api#customer_object
36     """
37     if event.customer:
38         # As customers are tied to local users, djstripe will not create
39         # customers that do not already exist locally.
40         _handle_crud_like_event(
41             target_cls=models.Customer, event=event, crud_exact=True, crud_valid=True
42         )
43 
44 
45 @webhooks.handler("customer.discount")
46 def customer_discount_webhook_handler(event):
47     """Handle updates to customer discount objects.
48 
49     Docs: https://stripe.com/docs/api#discounts
50 
51     Because there is no concept of a "Discount" model in dj-stripe (due to the
52     lack of a stripe id on them), this is a little different to the other
53     handlers.
54     """
55 
56     crud_type = CrudType.determine(event=event)
57     discount_data = event.data.get("object", {})
58     coupon_data = discount_data.get("coupon", {})
59     customer = event.customer
60 
61     if crud_type.created or crud_type.updated:
62         coupon, _ = _handle_crud_like_event(
63             target_cls=models.Coupon,
64             event=event,
65             data=coupon_data,
66             id=coupon_data.get("id"),
67         )
68         coupon_start = discount_data.get("start")
69         coupon_end = discount_data.get("end")
70     else:
71         coupon = None
72         coupon_start = None
73         coupon_end = None
74 
75     customer.coupon = coupon
76     customer.coupon_start = convert_tstamp(coupon_start)
77     customer.coupon_end = convert_tstamp(coupon_end)
78     customer.save()
79 
80 
81 @webhooks.handler("customer.source")
82 def customer_source_webhook_handler(event):
83     """Handle updates to customer payment-source objects.
84 
85     Docs: https://stripe.com/docs/api#customer_object-sources.
86     """
87     customer_data = event.data.get("object", {})
88     source_type = customer_data.get("object", {})
89 
90     # TODO: handle other types of sources
91     #  (https://stripe.com/docs/api#customer_object-sources)
92     if source_type == SourceType.card:
93         if event.verb.endswith("deleted") and customer_data:
94             # On customer.source.deleted, we do not delete the object,
95             # we merely unlink it.
96             # customer = Customer.objects.get(id=customer_data["id"])
97             # NOTE: for now, customer.sources still points to Card
98             # Also, https://github.com/dj-stripe/dj-stripe/issues/576
99             models.Card.objects.filter(id=customer_data.get("id", "")).delete()
100             models.DjstripePaymentMethod.objects.filter(
101                 id=customer_data.get("id", "")
102             ).delete()
103         else:
104             _handle_crud_like_event(target_cls=models.Card, event=event)
105 
106 
107 @webhooks.handler("customer.subscription")
108 def customer_subscription_webhook_handler(event):
109     """Handle updates to customer subscription objects.
110 
111     Docs an example subscription webhook response:
112     https://stripe.com/docs/api#subscription_object
113     """
114     _handle_crud_like_event(target_cls=models.Subscription, event=event)
115 
116 
117 @webhooks.handler("payment_method")
118 def payment_method_handler(event):
119     """
120     Handle updates to payment_method objects
121     :param event:
122     :return:
123 
124     Docs for:
125     - payment_method: https://stripe.com/docs/api/payment_methods
126     """
127     id_ = event.data.get("object", {}).get("id", None)
128 
129     if (
130         event.parts == ["payment_method", "detached"]
131         and id_
132         and id_.startswith("card_")
133     ):
134         # Special case to handle a quirk in stripe's wrapping of legacy "card" objects
135         # with payment_methods - card objects are deleted on detach, so treat this as
136         # a delete event
137         _handle_crud_like_event(
138             target_cls=models.PaymentMethod,
139             event=event,
140             crud_type=CrudType(deleted=True),
141         )
142     else:
143         _handle_crud_like_event(target_cls=models.PaymentMethod, event=event)
144 
145 
146 @webhooks.handler(
147     "transfer",
148     "charge",
149     "coupon",
150     "invoice",
151     "invoiceitem",
152     "payment_intent",
153     "plan",
154     "product",
155     "setup_intent",
156     "source",
157 )
158 def other_object_webhook_handler(event):
159     """
160     Handle updates to transfer, charge, coupon, invoice, invoiceitem, payment_intent,
161     plan, product, setup_intent and source objects.
162 
163     Docs for:
164     - charge: https://stripe.com/docs/api#charges
165     - coupon: https://stripe.com/docs/api#coupons
166     - invoice: https://stripe.com/docs/api#invoices
167     - invoiceitem: https://stripe.com/docs/api#invoiceitems
168     - plan: https://stripe.com/docs/api#plans
169     - product: https://stripe.com/docs/api#products
170     - source: https://stripe.com/docs/api#sources
171     - payment_intent: https://stripe.com/docs/api/payment_intents
172     """
173 
174     if event.parts[:2] == ["charge", "dispute"]:
175         # Do not attempt to handle charge.dispute.* events.
176         # We do not have a Dispute model yet.
177         target_cls = models.Dispute
178     else:
179         target_cls = {
180             "charge": models.Charge,
181             "coupon": models.Coupon,
182             "invoice": models.Invoice,
183             "invoiceitem": models.InvoiceItem,
184             "payment_intent": models.PaymentIntent,
185             "plan": models.Plan,
186             "product": models.Product,
187             "transfer": models.Transfer,
188             "setup_intent": models.SetupIntent,
189             "source": models.Source,
190         }.get(event.category)
191 
192     _handle_crud_like_event(target_cls=target_cls, event=event)
193 
194 
195 #
196 # Helpers
197 #
198 
199 
200 class CrudType(object):
201     """Helper object to determine CRUD-like event state."""
202 
203     created = False
204     updated = False
205     deleted = False
206 
207     def __init__(self, **kwargs):
208         """Set attributes."""
209         for k, v in kwargs.items():
210             setattr(self, k, v)
211 
212     @property
213     def valid(self):
214         """Return True if this is a CRUD-like event."""
215         return self.created or self.updated or self.deleted
216 
217     @classmethod
218     def determine(cls, event, verb=None, exact=False):
219         """
220         Determine if the event verb is a crud_type (without the 'R') event.
221 
222         :param event:
223         :type event: models.Event
224         :param verb: The event verb to examine.
225         :type verb: str
226         :param exact: If True, match crud_type to event verb string exactly.
227         :type exact: bool
228         :returns: The CrudType state object.
229         :rtype: CrudType
230         """
231         verb = verb or event.verb
232 
233         def check(crud_type_event):
234             if exact:
235                 return verb == crud_type_event
236             else:
237                 return verb.endswith(crud_type_event)
238 
239         created = updated = deleted = False
240 
241         if check("updated"):
242             updated = True
243         elif check("created"):
244             created = True
245         elif check("deleted"):
246             deleted = True
247 
248         return cls(created=created, updated=updated, deleted=deleted)
249 
250 
251 def _handle_crud_like_event(
252     target_cls,
253     event,
254     data=None,
255     verb=None,
256     id=None,
257     customer=None,
258     crud_type=None,
259     crud_exact=False,
260     crud_valid=False,
261 ):
262     """
263     Helper to process crud_type-like events for objects.
264 
265     Non-deletes (creates, updates and "anything else" events) are treated as
266     update_or_create events - The object will be retrieved locally, then it is
267     synchronised with the Stripe API for parity.
268 
269     Deletes only occur for delete events and cause the object to be deleted
270     from the local database, if it existed.  If it doesn't exist then it is
271     ignored (but the event processing still succeeds).
272 
273     :param target_cls: The djstripe model being handled.
274     :type target_cls: Type[models.StripeModel]
275     :param event: The event object
276     :type event: models.Event
277     :param data: The event object data (defaults to ``event.data``).
278     :param verb: The event verb (defaults to ``event.verb``).
279     :type verb: str
280     :param id: The object Stripe ID (defaults to ``object.id``).
281     :type id: str
282     :param customer: The customer object (defaults to ``event.customer``).
283     :param crud_type: The CrudType object (determined by default).
284     :param crud_exact: If True, match verb against CRUD type exactly.
285     :param crud_valid: If True, CRUD type must match valid type.
286     :returns: The object (if any) and the event CrudType.
287     :rtype: Tuple[models.StripeModel, CrudType]
288     """
289     data = data or event.data
290     id = id or data.get("object", {}).get("id", None)
291 
292     if not id:
293         # We require an object when applying CRUD-like events, so if there's
294         # no ID the event is ignored/dropped. This happens in events such as
295         # invoice.upcoming, which refer to a future (non-existant) invoice.
296         logger.debug(
297             "Ignoring %r Stripe event without object ID: %r", event.type, event
298         )
299         return
300 
301     verb = verb or event.verb
302     customer = customer or event.customer
303     crud_type = crud_type or CrudType.determine(
304         event=event, verb=verb, exact=crud_exact
305     )
306     obj = None
307 
308     if crud_valid and not crud_type.valid:
309         logger.debug(
310             "Ignoring %r Stripe event without valid CRUD type: %r", event.type, event
311         )
312         return
313 
314     if crud_type.deleted:
315         qs = target_cls.objects.filter(id=id)
316         if target_cls is models.Customer and qs.exists():
317             qs.get().purge()
318         else:
319             obj = target_cls.objects.filter(id=id).delete()
320     else:
321         # Any other event type (creates, updates, etc.) - This can apply to
322         # verbs that aren't strictly CRUD but Stripe do intend an update.  Such
323         # as invoice.payment_failed.
324         kwargs = {"id": id}
325         if hasattr(target_cls, "customer"):
326             kwargs["customer"] = customer
327         data = target_cls(**kwargs).api_retrieve()
328         obj = target_cls.sync_from_stripe_data(data)
329 
330     return obj, crud_type
331
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
     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")

{"golden_diff": "diff --git a/djstripe/event_handlers.py b/djstripe/event_handlers.py\n--- a/djstripe/event_handlers.py\n+++ b/djstripe/event_handlers.py\n@@ -111,7 +111,16 @@\n     Docs an example subscription webhook response:\n     https://stripe.com/docs/api#subscription_object\n     \"\"\"\n-    _handle_crud_like_event(target_cls=models.Subscription, event=event)\n+\n+    # customer.subscription.deleted doesn't actually delete the subscription\n+    # on the stripe side, it updates it to canceled status, so override\n+    # crud_type to update to match.\n+    crud_type = CrudType.determine(event=event)\n+    if crud_type.deleted:\n+        crud_type = CrudType(updated=True)\n+    _handle_crud_like_event(\n+        target_cls=models.Subscription, event=event, crud_type=crud_type\n+    )\n \n \n @webhooks.handler(\"payment_method\")\n", "issue": "Cancelling a Subscription removes the model instance\nI 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.\n", "before_files": [{"content": "\"\"\"\nWebhook event handlers for the various models\n\nStripe docs for Events: https://stripe.com/docs/api/events\nStripe docs for Webhooks: https://stripe.com/docs/webhooks\n\nTODO: Implement webhook event handlers for all the models that need to\n      respond to webhook events.\n\nNOTE:\n    Event data is not guaranteed to be in the correct API version format.\n    See #116. When writing a webhook handler, make sure to first\n    re-retrieve the object you wish to process.\n\n\"\"\"\nimport logging\n\nfrom . import models, webhooks\nfrom .enums import SourceType\nfrom .utils import convert_tstamp\n\nlogger = logging.getLogger(__name__)\n\n\n@webhooks.handler(\"customer\")\ndef customer_webhook_handler(event):\n    \"\"\"Handle updates to customer objects.\n\n    First determines the crud_type and then handles the event if a customer\n    exists locally.\n    As customers are tied to local users, djstripe will not create customers that\n    do not already exist locally.\n\n    Docs and an example customer webhook response:\n    https://stripe.com/docs/api#customer_object\n    \"\"\"\n    if event.customer:\n        # As customers are tied to local users, djstripe will not create\n        # customers that do not already exist locally.\n        _handle_crud_like_event(\n            target_cls=models.Customer, event=event, crud_exact=True, crud_valid=True\n        )\n\n\n@webhooks.handler(\"customer.discount\")\ndef customer_discount_webhook_handler(event):\n    \"\"\"Handle updates to customer discount objects.\n\n    Docs: https://stripe.com/docs/api#discounts\n\n    Because there is no concept of a \"Discount\" model in dj-stripe (due to the\n    lack of a stripe id on them), this is a little different to the other\n    handlers.\n    \"\"\"\n\n    crud_type = CrudType.determine(event=event)\n    discount_data = event.data.get(\"object\", {})\n    coupon_data = discount_data.get(\"coupon\", {})\n    customer = event.customer\n\n    if crud_type.created or crud_type.updated:\n        coupon, _ = _handle_crud_like_event(\n            target_cls=models.Coupon,\n            event=event,\n            data=coupon_data,\n            id=coupon_data.get(\"id\"),\n        )\n        coupon_start = discount_data.get(\"start\")\n        coupon_end = discount_data.get(\"end\")\n    else:\n        coupon = None\n        coupon_start = None\n        coupon_end = None\n\n    customer.coupon = coupon\n    customer.coupon_start = convert_tstamp(coupon_start)\n    customer.coupon_end = convert_tstamp(coupon_end)\n    customer.save()\n\n\n@webhooks.handler(\"customer.source\")\ndef customer_source_webhook_handler(event):\n    \"\"\"Handle updates to customer payment-source objects.\n\n    Docs: https://stripe.com/docs/api#customer_object-sources.\n    \"\"\"\n    customer_data = event.data.get(\"object\", {})\n    source_type = customer_data.get(\"object\", {})\n\n    # TODO: handle other types of sources\n    #  (https://stripe.com/docs/api#customer_object-sources)\n    if source_type == SourceType.card:\n        if event.verb.endswith(\"deleted\") and customer_data:\n            # On customer.source.deleted, we do not delete the object,\n            # we merely unlink it.\n            # customer = Customer.objects.get(id=customer_data[\"id\"])\n            # NOTE: for now, customer.sources still points to Card\n            # Also, https://github.com/dj-stripe/dj-stripe/issues/576\n            models.Card.objects.filter(id=customer_data.get(\"id\", \"\")).delete()\n            models.DjstripePaymentMethod.objects.filter(\n                id=customer_data.get(\"id\", \"\")\n            ).delete()\n        else:\n            _handle_crud_like_event(target_cls=models.Card, event=event)\n\n\n@webhooks.handler(\"customer.subscription\")\ndef customer_subscription_webhook_handler(event):\n    \"\"\"Handle updates to customer subscription objects.\n\n    Docs an example subscription webhook response:\n    https://stripe.com/docs/api#subscription_object\n    \"\"\"\n    _handle_crud_like_event(target_cls=models.Subscription, event=event)\n\n\n@webhooks.handler(\"payment_method\")\ndef payment_method_handler(event):\n    \"\"\"\n    Handle updates to payment_method objects\n    :param event:\n    :return:\n\n    Docs for:\n    - payment_method: https://stripe.com/docs/api/payment_methods\n    \"\"\"\n    id_ = event.data.get(\"object\", {}).get(\"id\", None)\n\n    if (\n        event.parts == [\"payment_method\", \"detached\"]\n        and id_\n        and id_.startswith(\"card_\")\n    ):\n        # Special case to handle a quirk in stripe's wrapping of legacy \"card\" objects\n        # with payment_methods - card objects are deleted on detach, so treat this as\n        # a delete event\n        _handle_crud_like_event(\n            target_cls=models.PaymentMethod,\n            event=event,\n            crud_type=CrudType(deleted=True),\n        )\n    else:\n        _handle_crud_like_event(target_cls=models.PaymentMethod, event=event)\n\n\n@webhooks.handler(\n    \"transfer\",\n    \"charge\",\n    \"coupon\",\n    \"invoice\",\n    \"invoiceitem\",\n    \"payment_intent\",\n    \"plan\",\n    \"product\",\n    \"setup_intent\",\n    \"source\",\n)\ndef other_object_webhook_handler(event):\n    \"\"\"\n    Handle updates to transfer, charge, coupon, invoice, invoiceitem, payment_intent,\n    plan, product, setup_intent and source objects.\n\n    Docs for:\n    - charge: https://stripe.com/docs/api#charges\n    - coupon: https://stripe.com/docs/api#coupons\n    - invoice: https://stripe.com/docs/api#invoices\n    - invoiceitem: https://stripe.com/docs/api#invoiceitems\n    - plan: https://stripe.com/docs/api#plans\n    - product: https://stripe.com/docs/api#products\n    - source: https://stripe.com/docs/api#sources\n    - payment_intent: https://stripe.com/docs/api/payment_intents\n    \"\"\"\n\n    if event.parts[:2] == [\"charge\", \"dispute\"]:\n        # Do not attempt to handle charge.dispute.* events.\n        # We do not have a Dispute model yet.\n        target_cls = models.Dispute\n    else:\n        target_cls = {\n            \"charge\": models.Charge,\n            \"coupon\": models.Coupon,\n            \"invoice\": models.Invoice,\n            \"invoiceitem\": models.InvoiceItem,\n            \"payment_intent\": models.PaymentIntent,\n            \"plan\": models.Plan,\n            \"product\": models.Product,\n            \"transfer\": models.Transfer,\n            \"setup_intent\": models.SetupIntent,\n            \"source\": models.Source,\n        }.get(event.category)\n\n    _handle_crud_like_event(target_cls=target_cls, event=event)\n\n\n#\n# Helpers\n#\n\n\nclass CrudType(object):\n    \"\"\"Helper object to determine CRUD-like event state.\"\"\"\n\n    created = False\n    updated = False\n    deleted = False\n\n    def __init__(self, **kwargs):\n        \"\"\"Set attributes.\"\"\"\n        for k, v in kwargs.items():\n            setattr(self, k, v)\n\n    @property\n    def valid(self):\n        \"\"\"Return True if this is a CRUD-like event.\"\"\"\n        return self.created or self.updated or self.deleted\n\n    @classmethod\n    def determine(cls, event, verb=None, exact=False):\n        \"\"\"\n        Determine if the event verb is a crud_type (without the 'R') event.\n\n        :param event:\n        :type event: models.Event\n        :param verb: The event verb to examine.\n        :type verb: str\n        :param exact: If True, match crud_type to event verb string exactly.\n        :type exact: bool\n        :returns: The CrudType state object.\n        :rtype: CrudType\n        \"\"\"\n        verb = verb or event.verb\n\n        def check(crud_type_event):\n            if exact:\n                return verb == crud_type_event\n            else:\n                return verb.endswith(crud_type_event)\n\n        created = updated = deleted = False\n\n        if check(\"updated\"):\n            updated = True\n        elif check(\"created\"):\n            created = True\n        elif check(\"deleted\"):\n            deleted = True\n\n        return cls(created=created, updated=updated, deleted=deleted)\n\n\ndef _handle_crud_like_event(\n    target_cls,\n    event,\n    data=None,\n    verb=None,\n    id=None,\n    customer=None,\n    crud_type=None,\n    crud_exact=False,\n    crud_valid=False,\n):\n    \"\"\"\n    Helper to process crud_type-like events for objects.\n\n    Non-deletes (creates, updates and \"anything else\" events) are treated as\n    update_or_create events - The object will be retrieved locally, then it is\n    synchronised with the Stripe API for parity.\n\n    Deletes only occur for delete events and cause the object to be deleted\n    from the local database, if it existed.  If it doesn't exist then it is\n    ignored (but the event processing still succeeds).\n\n    :param target_cls: The djstripe model being handled.\n    :type target_cls: Type[models.StripeModel]\n    :param event: The event object\n    :type event: models.Event\n    :param data: The event object data (defaults to ``event.data``).\n    :param verb: The event verb (defaults to ``event.verb``).\n    :type verb: str\n    :param id: The object Stripe ID (defaults to ``object.id``).\n    :type id: str\n    :param customer: The customer object (defaults to ``event.customer``).\n    :param crud_type: The CrudType object (determined by default).\n    :param crud_exact: If True, match verb against CRUD type exactly.\n    :param crud_valid: If True, CRUD type must match valid type.\n    :returns: The object (if any) and the event CrudType.\n    :rtype: Tuple[models.StripeModel, CrudType]\n    \"\"\"\n    data = data or event.data\n    id = id or data.get(\"object\", {}).get(\"id\", None)\n\n    if not id:\n        # We require an object when applying CRUD-like events, so if there's\n        # no ID the event is ignored/dropped. This happens in events such as\n        # invoice.upcoming, which refer to a future (non-existant) invoice.\n        logger.debug(\n            \"Ignoring %r Stripe event without object ID: %r\", event.type, event\n        )\n        return\n\n    verb = verb or event.verb\n    customer = customer or event.customer\n    crud_type = crud_type or CrudType.determine(\n        event=event, verb=verb, exact=crud_exact\n    )\n    obj = None\n\n    if crud_valid and not crud_type.valid:\n        logger.debug(\n            \"Ignoring %r Stripe event without valid CRUD type: %r\", event.type, event\n        )\n        return\n\n    if crud_type.deleted:\n        qs = target_cls.objects.filter(id=id)\n        if target_cls is models.Customer and qs.exists():\n            qs.get().purge()\n        else:\n            obj = target_cls.objects.filter(id=id).delete()\n    else:\n        # Any other event type (creates, updates, etc.) - This can apply to\n        # verbs that aren't strictly CRUD but Stripe do intend an update.  Such\n        # as invoice.payment_failed.\n        kwargs = {\"id\": id}\n        if hasattr(target_cls, \"customer\"):\n            kwargs[\"customer\"] = customer\n        data = target_cls(**kwargs).api_retrieve()\n        obj = target_cls.sync_from_stripe_data(data)\n\n    return obj, crud_type\n", "path": "djstripe/event_handlers.py"}], "after_files": [{"content": "\"\"\"\nWebhook event handlers for the various models\n\nStripe docs for Events: https://stripe.com/docs/api/events\nStripe docs for Webhooks: https://stripe.com/docs/webhooks\n\nTODO: Implement webhook event handlers for all the models that need to\n      respond to webhook events.\n\nNOTE:\n    Event data is not guaranteed to be in the correct API version format.\n    See #116. When writing a webhook handler, make sure to first\n    re-retrieve the object you wish to process.\n\n\"\"\"\nimport logging\n\nfrom . import models, webhooks\nfrom .enums import SourceType\nfrom .utils import convert_tstamp\n\nlogger = logging.getLogger(__name__)\n\n\n@webhooks.handler(\"customer\")\ndef customer_webhook_handler(event):\n    \"\"\"Handle updates to customer objects.\n\n    First determines the crud_type and then handles the event if a customer\n    exists locally.\n    As customers are tied to local users, djstripe will not create customers that\n    do not already exist locally.\n\n    Docs and an example customer webhook response:\n    https://stripe.com/docs/api#customer_object\n    \"\"\"\n    if event.customer:\n        # As customers are tied to local users, djstripe will not create\n        # customers that do not already exist locally.\n        _handle_crud_like_event(\n            target_cls=models.Customer, event=event, crud_exact=True, crud_valid=True\n        )\n\n\n@webhooks.handler(\"customer.discount\")\ndef customer_discount_webhook_handler(event):\n    \"\"\"Handle updates to customer discount objects.\n\n    Docs: https://stripe.com/docs/api#discounts\n\n    Because there is no concept of a \"Discount\" model in dj-stripe (due to the\n    lack of a stripe id on them), this is a little different to the other\n    handlers.\n    \"\"\"\n\n    crud_type = CrudType.determine(event=event)\n    discount_data = event.data.get(\"object\", {})\n    coupon_data = discount_data.get(\"coupon\", {})\n    customer = event.customer\n\n    if crud_type.created or crud_type.updated:\n        coupon, _ = _handle_crud_like_event(\n            target_cls=models.Coupon,\n            event=event,\n            data=coupon_data,\n            id=coupon_data.get(\"id\"),\n        )\n        coupon_start = discount_data.get(\"start\")\n        coupon_end = discount_data.get(\"end\")\n    else:\n        coupon = None\n        coupon_start = None\n        coupon_end = None\n\n    customer.coupon = coupon\n    customer.coupon_start = convert_tstamp(coupon_start)\n    customer.coupon_end = convert_tstamp(coupon_end)\n    customer.save()\n\n\n@webhooks.handler(\"customer.source\")\ndef customer_source_webhook_handler(event):\n    \"\"\"Handle updates to customer payment-source objects.\n\n    Docs: https://stripe.com/docs/api#customer_object-sources.\n    \"\"\"\n    customer_data = event.data.get(\"object\", {})\n    source_type = customer_data.get(\"object\", {})\n\n    # TODO: handle other types of sources\n    #  (https://stripe.com/docs/api#customer_object-sources)\n    if source_type == SourceType.card:\n        if event.verb.endswith(\"deleted\") and customer_data:\n            # On customer.source.deleted, we do not delete the object,\n            # we merely unlink it.\n            # customer = Customer.objects.get(id=customer_data[\"id\"])\n            # NOTE: for now, customer.sources still points to Card\n            # Also, https://github.com/dj-stripe/dj-stripe/issues/576\n            models.Card.objects.filter(id=customer_data.get(\"id\", \"\")).delete()\n            models.DjstripePaymentMethod.objects.filter(\n                id=customer_data.get(\"id\", \"\")\n            ).delete()\n        else:\n            _handle_crud_like_event(target_cls=models.Card, event=event)\n\n\n@webhooks.handler(\"customer.subscription\")\ndef customer_subscription_webhook_handler(event):\n    \"\"\"Handle updates to customer subscription objects.\n\n    Docs an example subscription webhook response:\n    https://stripe.com/docs/api#subscription_object\n    \"\"\"\n\n    # customer.subscription.deleted doesn't actually delete the subscription\n    # on the stripe side, it updates it to canceled status, so override\n    # crud_type to update to match.\n    crud_type = CrudType.determine(event=event)\n    if crud_type.deleted:\n        crud_type = CrudType(updated=True)\n    _handle_crud_like_event(\n        target_cls=models.Subscription, event=event, crud_type=crud_type\n    )\n\n\n@webhooks.handler(\"payment_method\")\ndef payment_method_handler(event):\n    \"\"\"\n    Handle updates to payment_method objects\n    :param event:\n    :return:\n\n    Docs for:\n    - payment_method: https://stripe.com/docs/api/payment_methods\n    \"\"\"\n    id_ = event.data.get(\"object\", {}).get(\"id\", None)\n\n    if (\n        event.parts == [\"payment_method\", \"detached\"]\n        and id_\n        and id_.startswith(\"card_\")\n    ):\n        # Special case to handle a quirk in stripe's wrapping of legacy \"card\" objects\n        # with payment_methods - card objects are deleted on detach, so treat this as\n        # a delete event\n        _handle_crud_like_event(\n            target_cls=models.PaymentMethod,\n            event=event,\n            crud_type=CrudType(deleted=True),\n        )\n    else:\n        _handle_crud_like_event(target_cls=models.PaymentMethod, event=event)\n\n\n@webhooks.handler(\n    \"transfer\",\n    \"charge\",\n    \"coupon\",\n    \"invoice\",\n    \"invoiceitem\",\n    \"payment_intent\",\n    \"plan\",\n    \"product\",\n    \"setup_intent\",\n    \"source\",\n)\ndef other_object_webhook_handler(event):\n    \"\"\"\n    Handle updates to transfer, charge, coupon, invoice, invoiceitem, payment_intent,\n    plan, product, setup_intent and source objects.\n\n    Docs for:\n    - charge: https://stripe.com/docs/api#charges\n    - coupon: https://stripe.com/docs/api#coupons\n    - invoice: https://stripe.com/docs/api#invoices\n    - invoiceitem: https://stripe.com/docs/api#invoiceitems\n    - plan: https://stripe.com/docs/api#plans\n    - product: https://stripe.com/docs/api#products\n    - source: https://stripe.com/docs/api#sources\n    - payment_intent: https://stripe.com/docs/api/payment_intents\n    \"\"\"\n\n    if event.parts[:2] == [\"charge\", \"dispute\"]:\n        # Do not attempt to handle charge.dispute.* events.\n        # We do not have a Dispute model yet.\n        target_cls = models.Dispute\n    else:\n        target_cls = {\n            \"charge\": models.Charge,\n            \"coupon\": models.Coupon,\n            \"invoice\": models.Invoice,\n            \"invoiceitem\": models.InvoiceItem,\n            \"payment_intent\": models.PaymentIntent,\n            \"plan\": models.Plan,\n            \"product\": models.Product,\n            \"transfer\": models.Transfer,\n            \"setup_intent\": models.SetupIntent,\n            \"source\": models.Source,\n        }.get(event.category)\n\n    _handle_crud_like_event(target_cls=target_cls, event=event)\n\n\n#\n# Helpers\n#\n\n\nclass CrudType(object):\n    \"\"\"Helper object to determine CRUD-like event state.\"\"\"\n\n    created = False\n    updated = False\n    deleted = False\n\n    def __init__(self, **kwargs):\n        \"\"\"Set attributes.\"\"\"\n        for k, v in kwargs.items():\n            setattr(self, k, v)\n\n    @property\n    def valid(self):\n        \"\"\"Return True if this is a CRUD-like event.\"\"\"\n        return self.created or self.updated or self.deleted\n\n    @classmethod\n    def determine(cls, event, verb=None, exact=False):\n        \"\"\"\n        Determine if the event verb is a crud_type (without the 'R') event.\n\n        :param event:\n        :type event: models.Event\n        :param verb: The event verb to examine.\n        :type verb: str\n        :param exact: If True, match crud_type to event verb string exactly.\n        :type exact: bool\n        :returns: The CrudType state object.\n        :rtype: CrudType\n        \"\"\"\n        verb = verb or event.verb\n\n        def check(crud_type_event):\n            if exact:\n                return verb == crud_type_event\n            else:\n                return verb.endswith(crud_type_event)\n\n        created = updated = deleted = False\n\n        if check(\"updated\"):\n            updated = True\n        elif check(\"created\"):\n            created = True\n        elif check(\"deleted\"):\n            deleted = True\n\n        return cls(created=created, updated=updated, deleted=deleted)\n\n\ndef _handle_crud_like_event(\n    target_cls,\n    event,\n    data=None,\n    verb=None,\n    id=None,\n    customer=None,\n    crud_type=None,\n    crud_exact=False,\n    crud_valid=False,\n):\n    \"\"\"\n    Helper to process crud_type-like events for objects.\n\n    Non-deletes (creates, updates and \"anything else\" events) are treated as\n    update_or_create events - The object will be retrieved locally, then it is\n    synchronised with the Stripe API for parity.\n\n    Deletes only occur for delete events and cause the object to be deleted\n    from the local database, if it existed.  If it doesn't exist then it is\n    ignored (but the event processing still succeeds).\n\n    :param target_cls: The djstripe model being handled.\n    :type target_cls: Type[models.StripeModel]\n    :param event: The event object\n    :type event: models.Event\n    :param data: The event object data (defaults to ``event.data``).\n    :param verb: The event verb (defaults to ``event.verb``).\n    :type verb: str\n    :param id: The object Stripe ID (defaults to ``object.id``).\n    :type id: str\n    :param customer: The customer object (defaults to ``event.customer``).\n    :param crud_type: The CrudType object (determined by default).\n    :param crud_exact: If True, match verb against CRUD type exactly.\n    :param crud_valid: If True, CRUD type must match valid type.\n    :returns: The object (if any) and the event CrudType.\n    :rtype: Tuple[models.StripeModel, CrudType]\n    \"\"\"\n    data = data or event.data\n    id = id or data.get(\"object\", {}).get(\"id\", None)\n\n    if not id:\n        # We require an object when applying CRUD-like events, so if there's\n        # no ID the event is ignored/dropped. This happens in events such as\n        # invoice.upcoming, which refer to a future (non-existant) invoice.\n        logger.debug(\n            \"Ignoring %r Stripe event without object ID: %r\", event.type, event\n        )\n        return\n\n    verb = verb or event.verb\n    customer = customer or event.customer\n    crud_type = crud_type or CrudType.determine(\n        event=event, verb=verb, exact=crud_exact\n    )\n    obj = None\n\n    if crud_valid and not crud_type.valid:\n        logger.debug(\n            \"Ignoring %r Stripe event without valid CRUD type: %r\", event.type, event\n        )\n        return\n\n    if crud_type.deleted:\n        qs = target_cls.objects.filter(id=id)\n        if target_cls is models.Customer and qs.exists():\n            qs.get().purge()\n        else:\n            obj = target_cls.objects.filter(id=id).delete()\n    else:\n        # Any other event type (creates, updates, etc.) - This can apply to\n        # verbs that aren't strictly CRUD but Stripe do intend an update.  Such\n        # as invoice.payment_failed.\n        kwargs = {\"id\": id}\n        if hasattr(target_cls, \"customer\"):\n            kwargs[\"customer\"] = customer\n        data = target_cls(**kwargs).api_retrieve()\n        obj = target_cls.sync_from_stripe_data(data)\n\n    return obj, crud_type\n", "path": "djstripe/event_handlers.py"}]}