We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
PaymentMethod (and potentially other new SCA models) don't sync from web hooks
**Describe the bug**
I'm using 4a828a48092a3904094917776ce725ec9aa3fce5 (after the #914 merge) and trying to set up an SCA complaint flow though I'm encountering issues with the web hooks, they don't seem to sync.
The reason, I assume, is that the stripe object name in the event is "payment_method" while the name in the PR is "paymentmethod". I assume it's the same issue for the other new models.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a new payment method directly through the stripe API.
2. Wait for the webhook to be called

**Expected behavior**
The object should be created from the event but it isn't.

Example event:
```
{
  "object": {
    "id": "pm_1F8uMTHa6wE0PhFmIrAlCMuB",
    "object": "payment_method",
    "billing_details": {
      "address": {
        "city": null,
        "country": "GB",
        "line1": null,
        "line2": null,
        "postal_code": "W1",
        "state": null
      },
      "email": "me@etesync.com",
      "name": "Jeo",
      "phone": null
    },
    "card": {
      "brand": "visa",
      "checks": {
        "address_line1_check": null,
        "address_postal_code_check": "pass",
        "cvc_check": "pass"
      },
      "country": "US",
      "exp_month": 2,
      "exp_year": 2022,
      "fingerprint": "No6qZ6uMjc9xCthT",
      "funding": "credit",
      "generated_from": null,
      "last4": "4242",
      "three_d_secure_usage": {
        "supported": true
      },
      "wallet": null
    },
    "created": 1566157701,
    "customer": "cus_FXgo0IlbgH2cQS",
    "livemode": false,
    "metadata": {
    },
    "type": "card"
  }
}
```

**Environment**
- dj-stripe version: master at 4a828a48092a3904094917776ce725ec9aa3fce5
- Your Stripe account's default API version: [e.g. 2019-02-19 - shown as "default" on https://dashboard.stripe.com/developers]
- Database: Postgres
- Python version: 3.7.4
- Django version: 2.2.4

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

Yes

**Additional context**
Add any other context about the problem here.


--- 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     "paymentintent",
124     "paymentmethod",
125     "plan",
126     "product",
127     "setupintent",
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             "paymentintent": models.PaymentIntent,
158             "paymentmethod": models.PaymentMethod,
159             "plan": models.Plan,
160             "product": models.Product,
161             "transfer": models.Transfer,
162             "setupintent": 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 verb: The event verb to examine.
197         :type verb: string (``str``/`unicode``)
198         :param exact: If True, match crud_type to event verb string exactly.
199         :param type: ``bool``
200         :returns: The CrudType state object.
201         :rtype: ``CrudType``
202         """
203         verb = verb or event.verb
204 
205         def check(crud_type_event):
206             if exact:
207                 return verb == crud_type_event
208             else:
209                 return verb.endswith(crud_type_event)
210 
211         created = updated = deleted = False
212 
213         if check("updated"):
214             updated = True
215         elif check("created"):
216             created = True
217         elif check("deleted"):
218             deleted = True
219 
220         return cls(created=created, updated=updated, deleted=deleted)
221 
222 
223 def _handle_crud_like_event(
224     target_cls,
225     event,
226     data=None,
227     verb=None,
228     id=None,
229     customer=None,
230     crud_type=None,
231     crud_exact=False,
232     crud_valid=False,
233 ):
234     """
235     Helper to process crud_type-like events for objects.
236 
237     Non-deletes (creates, updates and "anything else" events) are treated as
238     update_or_create events - The object will be retrieved locally, then it is
239     synchronised with the Stripe API for parity.
240 
241     Deletes only occur for delete events and cause the object to be deleted
242     from the local database, if it existed.  If it doesn't exist then it is
243     ignored (but the event processing still succeeds).
244 
245     :param target_cls: The djstripe model being handled.
246     :type: ``djstripe.models.StripeModel``
247     :param data: The event object data (defaults to ``event.data``).
248     :param verb: The event verb (defaults to ``event.verb``).
249     :param id: The object Stripe ID (defaults to ``object.id``).
250     :param customer: The customer object (defaults to ``event.customer``).
251     :param crud_type: The CrudType object (determined by default).
252     :param crud_exact: If True, match verb against CRUD type exactly.
253     :param crud_valid: If True, CRUD type must match valid type.
254     :returns: The object (if any) and the event CrudType.
255     :rtype: ``tuple(obj, CrudType)``
256     """
257     data = data or event.data
258     id = id or data.get("object", {}).get("id", None)
259 
260     if not id:
261         # We require an object when applying CRUD-like events, so if there's
262         # no ID the event is ignored/dropped. This happens in events such as
263         # invoice.upcoming, which refer to a future (non-existant) invoice.
264         logger.debug(
265             "Ignoring %r Stripe event without object ID: %r", event.type, event
266         )
267         return
268 
269     verb = verb or event.verb
270     customer = customer or event.customer
271     crud_type = crud_type or CrudType.determine(
272         event=event, verb=verb, exact=crud_exact
273     )
274     obj = None
275 
276     if crud_valid and not crud_type.valid:
277         logger.debug(
278             "Ignoring %r Stripe event without valid CRUD type: %r", event.type, event
279         )
280         return
281 
282     if crud_type.deleted:
283         qs = target_cls.objects.filter(id=id)
284         if target_cls is models.Customer and qs.exists():
285             qs.get().purge()
286         else:
287             obj = target_cls.objects.filter(id=id).delete()
288     else:
289         # Any other event type (creates, updates, etc.) - This can apply to
290         # verbs that aren't strictly CRUD but Stripe do intend an update.  Such
291         # as invoice.payment_failed.
292         kwargs = {"id": id}
293         if hasattr(target_cls, "customer"):
294             kwargs["customer"] = customer
295         data = target_cls(**kwargs).api_retrieve()
296         obj = target_cls.sync_from_stripe_data(data)
297 
298     return obj, crud_type
299
```
--- 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
@@ -120,11 +120,11 @@
     "coupon",
     "invoice",
     "invoiceitem",
-    "paymentintent",
-    "paymentmethod",
+    "payment_intent",
+    "payment_method",
     "plan",
     "product",
-    "setupintent",
+    "setup_intent",
     "source",
 )
 def other_object_webhook_handler(event):
@@ -154,12 +154,12 @@
             "coupon": models.Coupon,
             "invoice": models.Invoice,
             "invoiceitem": models.InvoiceItem,
-            "paymentintent": models.PaymentIntent,
-            "paymentmethod": models.PaymentMethod,
+            "payment_intent": models.PaymentIntent,
+            "payment_method": models.PaymentMethod,
             "plan": models.Plan,
             "product": models.Product,
             "transfer": models.Transfer,
-            "setupintent": models.SetupIntent,
+            "setup_intent": models.SetupIntent,
             "source": models.Source,
         }.get(event.category)

{"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@@ -120,11 +120,11 @@\n     \"coupon\",\n     \"invoice\",\n     \"invoiceitem\",\n-    \"paymentintent\",\n-    \"paymentmethod\",\n+    \"payment_intent\",\n+    \"payment_method\",\n     \"plan\",\n     \"product\",\n-    \"setupintent\",\n+    \"setup_intent\",\n     \"source\",\n )\n def other_object_webhook_handler(event):\n@@ -154,12 +154,12 @@\n             \"coupon\": models.Coupon,\n             \"invoice\": models.Invoice,\n             \"invoiceitem\": models.InvoiceItem,\n-            \"paymentintent\": models.PaymentIntent,\n-            \"paymentmethod\": models.PaymentMethod,\n+            \"payment_intent\": models.PaymentIntent,\n+            \"payment_method\": models.PaymentMethod,\n             \"plan\": models.Plan,\n             \"product\": models.Product,\n             \"transfer\": models.Transfer,\n-            \"setupintent\": models.SetupIntent,\n+            \"setup_intent\": models.SetupIntent,\n             \"source\": models.Source,\n         }.get(event.category)\n", "issue": "PaymentMethod (and potentially other new SCA models) don't sync from web hooks\n**Describe the bug**\r\nI'm using 4a828a48092a3904094917776ce725ec9aa3fce5 (after the #914 merge) and trying to set up an SCA complaint flow though I'm encountering issues with the web hooks, they don't seem to sync.\r\nThe reason, I assume, is that the stripe object name in the event is \"payment_method\" while the name in the PR is \"paymentmethod\". I assume it's the same issue for the other new models.\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n1. Create a new payment method directly through the stripe API.\r\n2. Wait for the webhook to be called\r\n\r\n**Expected behavior**\r\nThe object should be created from the event but it isn't.\r\n\r\nExample event:\r\n```\r\n{\r\n  \"object\": {\r\n    \"id\": \"pm_1F8uMTHa6wE0PhFmIrAlCMuB\",\r\n    \"object\": \"payment_method\",\r\n    \"billing_details\": {\r\n      \"address\": {\r\n        \"city\": null,\r\n        \"country\": \"GB\",\r\n        \"line1\": null,\r\n        \"line2\": null,\r\n        \"postal_code\": \"W1\",\r\n        \"state\": null\r\n      },\r\n      \"email\": \"me@etesync.com\",\r\n      \"name\": \"Jeo\",\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\": \"pass\",\r\n        \"cvc_check\": \"pass\"\r\n      },\r\n      \"country\": \"US\",\r\n      \"exp_month\": 2,\r\n      \"exp_year\": 2022,\r\n      \"fingerprint\": \"No6qZ6uMjc9xCthT\",\r\n      \"funding\": \"credit\",\r\n      \"generated_from\": null,\r\n      \"last4\": \"4242\",\r\n      \"three_d_secure_usage\": {\r\n        \"supported\": true\r\n      },\r\n      \"wallet\": null\r\n    },\r\n    \"created\": 1566157701,\r\n    \"customer\": \"cus_FXgo0IlbgH2cQS\",\r\n    \"livemode\": false,\r\n    \"metadata\": {\r\n    },\r\n    \"type\": \"card\"\r\n  }\r\n}\r\n```\r\n\r\n**Environment**\r\n- dj-stripe version: master at 4a828a48092a3904094917776ce725ec9aa3fce5\r\n- Your Stripe account's default API version: [e.g. 2019-02-19 - shown as \"default\" on https://dashboard.stripe.com/developers]\r\n- Database: Postgres\r\n- Python version: 3.7.4\r\n- Django version: 2.2.4\r\n\r\n**Can you reproduce the issue with the latest version of master?**\r\n\r\nYes\r\n\r\n**Additional context**\r\nAdd any other context about the problem here.\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    \"paymentintent\",\n    \"paymentmethod\",\n    \"plan\",\n    \"product\",\n    \"setupintent\",\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            \"paymentintent\": models.PaymentIntent,\n            \"paymentmethod\": models.PaymentMethod,\n            \"plan\": models.Plan,\n            \"product\": models.Product,\n            \"transfer\": models.Transfer,\n            \"setupintent\": 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 verb: The event verb to examine.\n        :type verb: string (``str``/`unicode``)\n        :param exact: If True, match crud_type to event verb string exactly.\n        :param type: ``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: ``djstripe.models.StripeModel``\n    :param data: The event object data (defaults to ``event.data``).\n    :param verb: The event verb (defaults to ``event.verb``).\n    :param id: The object Stripe ID (defaults to ``object.id``).\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(obj, 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(\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 verb: The event verb to examine.\n        :type verb: string (``str``/`unicode``)\n        :param exact: If True, match crud_type to event verb string exactly.\n        :param type: ``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: ``djstripe.models.StripeModel``\n    :param data: The event object data (defaults to ``event.data``).\n    :param verb: The event verb (defaults to ``event.verb``).\n    :param id: The object Stripe ID (defaults to ``object.id``).\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(obj, 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"}]}