We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
PaymentMethod event incorrect behavior
**Describe the bug**
I encountered a new bug related to new models (#914 ) in current master.
At least `payment_method.*`-events are not working correctly with the general crud-style handling.

The payload can contain ie. card id instead of real PaymentMethod IDs and that means you can't currently retrieve them through stripe `/v1/payment_methods/:id`-api.

I also reported that to stripe (related IRC conversation: https://freenode.logbot.info/stripe/20190903#c2618918) because I think it's bug in implementation of either the events or then the API

Anyway the current implementation indicates that we should check out all the events related to those new models if they really are doing what they supposed to.
Preferably with real production environment, the data is not like in the documentation apparently.

**Example event causing problems**
```
{
  "id": "evt_1FEiXXXXXXXX",
  "object": "event",
  "api_version": "2018-11-08",
  "created": 1567541994,
  "data": {
    "object": {
      "id": "card_1DXXXXXXXXXXX",
      "object": "payment_method",
      "billing_details": {
        "address": {
          "city": null,
          "country": null,
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": null,
        "name": "Charles Customer",
        "phone": null
      },
      "card": {
        "brand": "visa",
        "checks": {
          "address_line1_check": null,
          "address_postal_code_check": null,
          "cvc_check": null
        },
        "country": "FR",
        "exp_month": 8,
        "exp_year": 2022,
        "fingerprint": "xxxxx",
        "funding": "debit",
        "generated_from": null,
        "last4": "2222",
        "three_d_secure_usage": {
          "supported": true
        },
        "wallet": null
      },
      "created": 1545493504,
      "customer": "cus_ECbxxxxxxxxxx",
      "livemode": true,
      "metadata": {
      },
      "type": "card"
    }
  },
  "livemode": true,
  "pending_webhooks": 1,
  "request": {
    "id": "req_j9PAxxxxxxxxx",
    "idempotency_key": null
  },
  "type": "payment_method.detached"
}
```
**Expected behavior**
Events should be handled with more specific handlers.

**Environment**
- dj-stripe version: [e.g. master at 0298f21e456b25232a6e47199db14cee18c9cfc1]

**Can you reproduce the issue with the latest version of master?**

Yes



--- 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(
118     "transfer",
119     "charge",
120     "coupon",
121     "invoice",
122     "invoiceitem",
123     "payment_intent",
124     "payment_method",
125     "plan",
126     "product",
127     "setup_intent",
128     "source",
129 )
130 def other_object_webhook_handler(event):
131     """
132     Handle updates to transfer, charge, invoice, invoiceitem, plan, product
133     and source objects.
134 
135     Docs for:
136     - charge: https://stripe.com/docs/api#charges
137     - coupon: https://stripe.com/docs/api#coupons
138     - invoice: https://stripe.com/docs/api#invoices
139     - invoiceitem: https://stripe.com/docs/api#invoiceitems
140     - plan: https://stripe.com/docs/api#plans
141     - product: https://stripe.com/docs/api#products
142     - source: https://stripe.com/docs/api#sources
143     - payment_method: https://stripe.com/docs/api/payment_methods
144     - payment_intent: https://stripe.com/docs/api/payment_intents
145     """
146 
147     if event.parts[:2] == ["charge", "dispute"]:
148         # Do not attempt to handle charge.dispute.* events.
149         # We do not have a Dispute model yet.
150         target_cls = models.Dispute
151     else:
152         target_cls = {
153             "charge": models.Charge,
154             "coupon": models.Coupon,
155             "invoice": models.Invoice,
156             "invoiceitem": models.InvoiceItem,
157             "payment_intent": models.PaymentIntent,
158             "payment_method": models.PaymentMethod,
159             "plan": models.Plan,
160             "product": models.Product,
161             "transfer": models.Transfer,
162             "setup_intent": models.SetupIntent,
163             "source": models.Source,
164         }.get(event.category)
165 
166     _handle_crud_like_event(target_cls=target_cls, event=event)
167 
168 
169 #
170 # Helpers
171 #
172 
173 
174 class CrudType(object):
175     """Helper object to determine CRUD-like event state."""
176 
177     created = False
178     updated = False
179     deleted = False
180 
181     def __init__(self, **kwargs):
182         """Set attributes."""
183         for k, v in kwargs.items():
184             setattr(self, k, v)
185 
186     @property
187     def valid(self):
188         """Return True if this is a CRUD-like event."""
189         return self.created or self.updated or self.deleted
190 
191     @classmethod
192     def determine(cls, event, verb=None, exact=False):
193         """
194         Determine if the event verb is a crud_type (without the 'R') event.
195 
196         :param event:
197         :type event: models.Event
198         :param verb: The event verb to examine.
199         :type verb: str
200         :param exact: If True, match crud_type to event verb string exactly.
201         :type exact: bool
202         :returns: The CrudType state object.
203         :rtype: CrudType
204         """
205         verb = verb or event.verb
206 
207         def check(crud_type_event):
208             if exact:
209                 return verb == crud_type_event
210             else:
211                 return verb.endswith(crud_type_event)
212 
213         created = updated = deleted = False
214 
215         if check("updated"):
216             updated = True
217         elif check("created"):
218             created = True
219         elif check("deleted"):
220             deleted = True
221 
222         return cls(created=created, updated=updated, deleted=deleted)
223 
224 
225 def _handle_crud_like_event(
226     target_cls,
227     event,
228     data=None,
229     verb=None,
230     id=None,
231     customer=None,
232     crud_type=None,
233     crud_exact=False,
234     crud_valid=False,
235 ):
236     """
237     Helper to process crud_type-like events for objects.
238 
239     Non-deletes (creates, updates and "anything else" events) are treated as
240     update_or_create events - The object will be retrieved locally, then it is
241     synchronised with the Stripe API for parity.
242 
243     Deletes only occur for delete events and cause the object to be deleted
244     from the local database, if it existed.  If it doesn't exist then it is
245     ignored (but the event processing still succeeds).
246 
247     :param target_cls: The djstripe model being handled.
248     :type target_cls: Type[models.StripeModel]
249     :param event: The event object
250     :type event: models.Event
251     :param data: The event object data (defaults to ``event.data``).
252     :param verb: The event verb (defaults to ``event.verb``).
253     :type verb: str
254     :param id: The object Stripe ID (defaults to ``object.id``).
255     :type id: str
256     :param customer: The customer object (defaults to ``event.customer``).
257     :param crud_type: The CrudType object (determined by default).
258     :param crud_exact: If True, match verb against CRUD type exactly.
259     :param crud_valid: If True, CRUD type must match valid type.
260     :returns: The object (if any) and the event CrudType.
261     :rtype: Tuple[models.StripeModel, CrudType]
262     """
263     data = data or event.data
264     id = id or data.get("object", {}).get("id", None)
265 
266     if not id:
267         # We require an object when applying CRUD-like events, so if there's
268         # no ID the event is ignored/dropped. This happens in events such as
269         # invoice.upcoming, which refer to a future (non-existant) invoice.
270         logger.debug(
271             "Ignoring %r Stripe event without object ID: %r", event.type, event
272         )
273         return
274 
275     verb = verb or event.verb
276     customer = customer or event.customer
277     crud_type = crud_type or CrudType.determine(
278         event=event, verb=verb, exact=crud_exact
279     )
280     obj = None
281 
282     if crud_valid and not crud_type.valid:
283         logger.debug(
284             "Ignoring %r Stripe event without valid CRUD type: %r", event.type, event
285         )
286         return
287 
288     if crud_type.deleted:
289         qs = target_cls.objects.filter(id=id)
290         if target_cls is models.Customer and qs.exists():
291             qs.get().purge()
292         else:
293             obj = target_cls.objects.filter(id=id).delete()
294     else:
295         # Any other event type (creates, updates, etc.) - This can apply to
296         # verbs that aren't strictly CRUD but Stripe do intend an update.  Such
297         # as invoice.payment_failed.
298         kwargs = {"id": id}
299         if hasattr(target_cls, "customer"):
300             kwargs["customer"] = customer
301         data = target_cls(**kwargs).api_retrieve()
302         obj = target_cls.sync_from_stripe_data(data)
303 
304     return obj, crud_type
305
```
--- 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
@@ -114,6 +114,35 @@
     _handle_crud_like_event(target_cls=models.Subscription, event=event)
 
 
+@webhooks.handler("payment_method")
+def payment_method_handler(event):
+    """
+    Handle updates to payment_method objects
+    :param event:
+    :return:
+
+    Docs for:
+    - payment_method: https://stripe.com/docs/api/payment_methods
+    """
+    id_ = event.data.get("object", {}).get("id", None)
+
+    if (
+        event.parts == ["payment_method", "detached"]
+        and id_
+        and id_.startswith("card_")
+    ):
+        # Special case to handle a quirk in stripe's wrapping of legacy "card" objects
+        # with payment_methods - card objects are deleted on detach, so treat this as
+        # a delete event
+        _handle_crud_like_event(
+            target_cls=models.PaymentMethod,
+            event=event,
+            crud_type=CrudType(deleted=True),
+        )
+    else:
+        _handle_crud_like_event(target_cls=models.PaymentMethod, event=event)
+
+
 @webhooks.handler(
     "transfer",
     "charge",
@@ -121,7 +150,6 @@
     "invoice",
     "invoiceitem",
     "payment_intent",
-    "payment_method",
     "plan",
     "product",
     "setup_intent",
@@ -129,8 +157,8 @@
 )
 def other_object_webhook_handler(event):
     """
-    Handle updates to transfer, charge, invoice, invoiceitem, plan, product
-    and source objects.
+    Handle updates to transfer, charge, coupon, invoice, invoiceitem, payment_intent,
+    plan, product, setup_intent and source objects.
 
     Docs for:
     - charge: https://stripe.com/docs/api#charges
@@ -140,7 +168,6 @@
     - plan: https://stripe.com/docs/api#plans
     - product: https://stripe.com/docs/api#products
     - source: https://stripe.com/docs/api#sources
-    - payment_method: https://stripe.com/docs/api/payment_methods
     - payment_intent: https://stripe.com/docs/api/payment_intents
     """
 
@@ -155,7 +182,6 @@
             "invoice": models.Invoice,
             "invoiceitem": models.InvoiceItem,
             "payment_intent": models.PaymentIntent,
-            "payment_method": models.PaymentMethod,
             "plan": models.Plan,
             "product": models.Product,
             "transfer": models.Transfer,

{"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@@ -114,6 +114,35 @@\n     _handle_crud_like_event(target_cls=models.Subscription, event=event)\n \n \n+@webhooks.handler(\"payment_method\")\n+def 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@@ -121,7 +150,6 @@\n     \"invoice\",\n     \"invoiceitem\",\n     \"payment_intent\",\n-    \"payment_method\",\n     \"plan\",\n     \"product\",\n     \"setup_intent\",\n@@ -129,8 +157,8 @@\n )\n def other_object_webhook_handler(event):\n     \"\"\"\n-    Handle updates to transfer, charge, invoice, invoiceitem, plan, product\n-    and source objects.\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@@ -140,7 +168,6 @@\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_method: https://stripe.com/docs/api/payment_methods\n     - payment_intent: https://stripe.com/docs/api/payment_intents\n     \"\"\"\n \n@@ -155,7 +182,6 @@\n             \"invoice\": models.Invoice,\n             \"invoiceitem\": models.InvoiceItem,\n             \"payment_intent\": models.PaymentIntent,\n-            \"payment_method\": models.PaymentMethod,\n             \"plan\": models.Plan,\n             \"product\": models.Product,\n             \"transfer\": models.Transfer,\n", "issue": "PaymentMethod event incorrect behavior\n**Describe the bug**\r\nI encountered a new bug related to new models (#914 ) in current master.\r\nAt least `payment_method.*`-events are not working correctly with the general crud-style handling.\r\n\r\nThe payload can contain ie. card id instead of real PaymentMethod IDs and that means you can't currently retrieve them through stripe `/v1/payment_methods/:id`-api.\r\n\r\nI also reported that to stripe (related IRC conversation: https://freenode.logbot.info/stripe/20190903#c2618918) because I think it's bug in implementation of either the events or then the API\r\n\r\nAnyway the current implementation indicates that we should check out all the events related to those new models if they really are doing what they supposed to.\r\nPreferably with real production environment, the data is not like in the documentation apparently.\r\n\r\n**Example event causing problems**\r\n```\r\n{\r\n  \"id\": \"evt_1FEiXXXXXXXX\",\r\n  \"object\": \"event\",\r\n  \"api_version\": \"2018-11-08\",\r\n  \"created\": 1567541994,\r\n  \"data\": {\r\n    \"object\": {\r\n      \"id\": \"card_1DXXXXXXXXXXX\",\r\n      \"object\": \"payment_method\",\r\n      \"billing_details\": {\r\n        \"address\": {\r\n          \"city\": null,\r\n          \"country\": null,\r\n          \"line1\": null,\r\n          \"line2\": null,\r\n          \"postal_code\": null,\r\n          \"state\": null\r\n        },\r\n        \"email\": null,\r\n        \"name\": \"Charles Customer\",\r\n        \"phone\": null\r\n      },\r\n      \"card\": {\r\n        \"brand\": \"visa\",\r\n        \"checks\": {\r\n          \"address_line1_check\": null,\r\n          \"address_postal_code_check\": null,\r\n          \"cvc_check\": null\r\n        },\r\n        \"country\": \"FR\",\r\n        \"exp_month\": 8,\r\n        \"exp_year\": 2022,\r\n        \"fingerprint\": \"xxxxx\",\r\n        \"funding\": \"debit\",\r\n        \"generated_from\": null,\r\n        \"last4\": \"2222\",\r\n        \"three_d_secure_usage\": {\r\n          \"supported\": true\r\n        },\r\n        \"wallet\": null\r\n      },\r\n      \"created\": 1545493504,\r\n      \"customer\": \"cus_ECbxxxxxxxxxx\",\r\n      \"livemode\": true,\r\n      \"metadata\": {\r\n      },\r\n      \"type\": \"card\"\r\n    }\r\n  },\r\n  \"livemode\": true,\r\n  \"pending_webhooks\": 1,\r\n  \"request\": {\r\n    \"id\": \"req_j9PAxxxxxxxxx\",\r\n    \"idempotency_key\": null\r\n  },\r\n  \"type\": \"payment_method.detached\"\r\n}\r\n```\r\n**Expected behavior**\r\nEvents should be handled with more specific handlers.\r\n\r\n**Environment**\r\n- dj-stripe version: [e.g. master at 0298f21e456b25232a6e47199db14cee18c9cfc1]\r\n\r\n**Can you reproduce the issue with the latest version of master?**\r\n\r\nYes\r\n\r\n\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(\n    \"transfer\",\n    \"charge\",\n    \"coupon\",\n    \"invoice\",\n    \"invoiceitem\",\n    \"payment_intent\",\n    \"payment_method\",\n    \"plan\",\n    \"product\",\n    \"setup_intent\",\n    \"source\",\n)\ndef other_object_webhook_handler(event):\n    \"\"\"\n    Handle updates to transfer, charge, invoice, invoiceitem, plan, product\n    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_method: https://stripe.com/docs/api/payment_methods\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            \"payment_method\": models.PaymentMethod,\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    _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"}]}