def _handle_crud_like_event(
    target_cls, event: "models.Event", data=None, id: str = None, crud_type=None
):
    """
    Helper to process crud_type-like events for objects.

    Non-deletes (creates, updates and "anything else" events) are treated as
    update_or_create events - The object will be retrieved locally, then it is
    synchronised with the Stripe API for parity.

    Deletes only occur for delete events and cause the object to be deleted
    from the local database, if it existed.  If it doesn't exist then it is
    ignored (but the event processing still succeeds).

    :param target_cls: The djstripe model being handled.
    :type target_cls: Type[models.StripeModel]
    :param event: The event object
    :param data: The event object data (defaults to ``event.data``).
    :param id: The object Stripe ID (defaults to ``object.id``).
    :param crud_type: The CrudType object (determined by default).
    :returns: The object (if any) and the event CrudType.
    :rtype: Tuple[models.StripeModel, CrudType]
    """
    data = data or event.data
    id = id or data.get("object", {}).get("id", None)
    stripe_account = getattr(event.djstripe_owner_account, "id", None)

    if not id:
        # We require an object when applying CRUD-like events, so if there's
        # no ID the event is ignored/dropped. This happens in events such as
        # invoice.upcoming, which refer to a future (non-existant) invoice.
        logger.debug("Ignoring Stripe event %r without object ID", event.id)
        return

    crud_type = crud_type or CrudType.determine(event=event, verb=event.verb)

    if crud_type is CrudType.DELETED:
        qs = target_cls.objects.filter(id=id)
        if target_cls is models.Customer and qs.exists():
            qs.get().purge()
            obj = None
        else:
            obj = target_cls.objects.filter(id=id).delete()
    else:
        # Any other event type (creates, updates, etc.) - This can apply to
        # verbs that aren't strictly CRUD but Stripe do intend an update.  Such
        # as invoice.payment_failed.
        kwargs = {"id": id}
        if hasattr(target_cls, "customer"):
            kwargs["customer"] = event.customer

        # For account.external_account.* events
        if event.parts[:2] == ["account", "external_account"] and stripe_account:
            kwargs["account"] = models.Account._get_or_retrieve(id=stripe_account)

        # Stripe doesn't allow direct retrieval of Discount Objects
        if target_cls != models.Discount:
            data = target_cls(**kwargs).api_retrieve(
                stripe_account=stripe_account, api_key=event.default_api_key
            )
        else:
            data = data.get("object")

        # create or update the object from the retrieved Stripe Data
        obj = target_cls.sync_from_stripe_data(data, api_key=event.default_api_key)

    return obj

    Helper to process crud_type-like events for objects.

    Non-deletes (creates, updates and "anything else" events) are treated as
    update_or_create events - The object will be retrieved locally, then it is
    synchronised with the Stripe API for parity.

    Deletes only occur for delete events and cause the object to be deleted
    from the local database, if it existed.  If it doesn't exist then it is
    ignored (but the event processing still succeeds).

    :param target_cls: The djstripe model being handled.
    :type target_cls: Type[models.StripeModel]
    :param event: The event object
    :param data: The event object data (defaults to ``event.data``).
    :param id: The object Stripe ID (defaults to ``object.id``).
    :param crud_type: The CrudType object (determined by default).
    :returns: The object (if any) and the event CrudType.
    :rtype: Tuple[models.StripeModel, CrudType]
    
djstripe/event_handlers.py