We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Webhook is not syncing TaxRate model
**Describe the bug**
Whenever I create or modify an existing TaxRate, the webhooks (signals tax_rate.created and tax_rate.updated) are triggered but the changes are not reflected in the database.

**To Reproduce**
Steps to reproduce the behavior:

1. Create a new TaxRate object using the Django shell:

```
import stripe
stripe.TaxRate.create(display_name='name', description='desc', jurisdiction='ES', percentage=21.0, inclusive=False)
```

2. Modify the created TaxRate object using the Stripe Dashboard

**Expected behavior**
Changes should be reflected in the database.

A possible workaround is running: `./manage.py djstripe_sync_models TaxRate`

**Environment**
- dj-stripe version: 2.2.1
- Your Stripe account's default API version: 2019-11-05
- Database: Postgres 10.10
- Python version: 3.6
- Django version: 2.2.7

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

{"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@@ -153,7 +153,6 @@\n \n \n @webhooks.handler(\n-    \"transfer\",\n     \"charge\",\n     \"coupon\",\n     \"invoice\",\n@@ -163,21 +162,26 @@\n     \"product\",\n     \"setup_intent\",\n     \"source\",\n+    \"tax_rate\",\n+    \"transfer\",\n )\n def 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+    Handle updates to charge, coupon, invoice, invoiceitem, payment_intent,\n+    plan, product, setup_intent, source, tax_rate and transfer 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+    - 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     - payment_intent: https://stripe.com/docs/api/payment_intents\n+    - plan: https://stripe.com/docs/api/plans\n+    - product: https://stripe.com/docs/api/products\n+    - setup_intent: https://stripe.com/docs/api/setup_intents\n+    - source: https://stripe.com/docs/api/sources\n+    - tax_rate: https://stripe.com/docs/api/tax_rates/\n+    - transfer: https://stripe.com/docs/api/transfers\n     \"\"\"\n \n     if event.parts[:2] == [\"charge\", \"dispute\"]:\n@@ -196,6 +200,7 @@\n             \"transfer\": models.Transfer,\n             \"setup_intent\": models.SetupIntent,\n             \"source\": models.Source,\n+            \"tax_rate\": models.TaxRate,\n         }.get(event.category)\n \n     _handle_crud_like_event(target_cls=target_cls, event=event)\n", "issue": "Webhook is not syncing TaxRate model\n**Describe the bug**\r\nWhenever I create or modify an existing TaxRate, the webhooks (signals tax_rate.created and tax_rate.updated) are triggered but the changes are not reflected in the database.\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n\r\n1. Create a new TaxRate object using the Django shell:\r\n\r\n```\r\nimport stripe\r\nstripe.TaxRate.create(display_name='name', description='desc', jurisdiction='ES', percentage=21.0, inclusive=False)\r\n```\r\n\r\n2. Modify the created TaxRate object using the Stripe Dashboard\r\n\r\n**Expected behavior**\r\nChanges should be reflected in the database.\r\n\r\nA possible workaround is running: `./manage.py djstripe_sync_models TaxRate`\r\n\r\n**Environment**\r\n- dj-stripe version: 2.2.1\r\n- Your Stripe account's default API version: 2019-11-05\r\n- Database: Postgres 10.10\r\n- Python version: 3.6\r\n- Django version: 2.2.7\r\n\r\n**Can you reproduce the issue with the latest version of master?**\r\n\r\nYes\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\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"}], "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    \"charge\",\n    \"coupon\",\n    \"invoice\",\n    \"invoiceitem\",\n    \"payment_intent\",\n    \"plan\",\n    \"product\",\n    \"setup_intent\",\n    \"source\",\n    \"tax_rate\",\n    \"transfer\",\n)\ndef other_object_webhook_handler(event):\n    \"\"\"\n    Handle updates to charge, coupon, invoice, invoiceitem, payment_intent,\n    plan, product, setup_intent, source, tax_rate and transfer 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    - payment_intent: https://stripe.com/docs/api/payment_intents\n    - plan: https://stripe.com/docs/api/plans\n    - product: https://stripe.com/docs/api/products\n    - setup_intent: https://stripe.com/docs/api/setup_intents\n    - source: https://stripe.com/docs/api/sources\n    - tax_rate: https://stripe.com/docs/api/tax_rates/\n    - transfer: https://stripe.com/docs/api/transfers\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            \"tax_rate\": models.TaxRate,\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"}]}