Skip to content

Commit

Permalink
Add management command for re-processing Events (dj-stripe#954)
Browse files Browse the repository at this point in the history
Resolves dj-stripe#89
  • Loading branch information
Ryan Causey authored and therefromhere committed Sep 3, 2019
1 parent 7254acc commit e8ae93c
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 1 deletion.
2 changes: 1 addition & 1 deletion HISTORY.rst
Expand Up @@ -16,7 +16,7 @@ History
- Dropped previously-deprecated properties ``Charge.source_type`` and ``Charge.source_stripe_id``
- ``enums.PaymentMethodType`` has been deprecated, use ``enums.DjstripePaymentMethodType``
- Made ``SubscriptionItem.quantity`` nullable as per Plans with ``usage_type="metered"`` (follow-up to #865)
- Added manage command ``djstripe_sync_models`` (#727, #89)
- Added manage commands ``djstripe_sync_models`` and ``djstripe_process_events`` (#727, #89)
- Fixed issue with re-creating a customer after `Customer.purge()` (#916)
- Fixed sync of Customer Bank Accounts (#829)
- New models
Expand Down
116 changes: 116 additions & 0 deletions djstripe/management/commands/djstripe_process_events.py
@@ -0,0 +1,116 @@
from django.core.management.base import BaseCommand

from ... import models
from ... import settings as djstripe_settings
from ...mixins import VerbosityAwareOutputMixin


class Command(VerbosityAwareOutputMixin, BaseCommand):
"""Command to process all Events.
Optional arguments are provided to limit the number of Events processed.
Note: this is only guaranteed go back at most 30 days based on the
current limitation of stripe's events API. See: https://stripe.com/docs/api/events
"""

help = (
"Process all Events. Use optional arguments to limit the Events to process. "
"Note: this is only guaranteed go back at most 30 days based on the current "
"limitation of stripe's events API. See: https://stripe.com/docs/api/events"
)

def add_arguments(self, parser):
"""Add optional arugments to filter Events by."""
# Use a mutually exclusive group to prevent multiple arguments being
# specified together.
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--ids",
nargs="*",
help="An optional space separated list of specific Event IDs to sync.",
)
group.add_argument(
"--failed",
action="store_true",
help="Syncs and processes only the events that have failed webhooks.",
)
group.add_argument(
"--type",
help=(
"A string containing a specific event name,"
" or group of events using * as a wildcard."
" The list will be filtered to include only"
" events with a matching event property."
),
)

def handle(self, *args, **options):
"""Try to process Events listed from the API."""
# Set the verbosity to determine how much we output, if at all.
self.set_verbosity(options)

event_ids = options["ids"]
failed = options["failed"]
type_filter = options["type"]

# Args are mutually exclusive,
# so output what we are doing based on that assumption.
if failed:
self.output("Processing all failed events")
elif type_filter:
self.output(
"Processing all events that match {filter}".format(filter=type_filter)
)
elif event_ids:
self.output("Processing specific events {events}".format(events=event_ids))
else:
self.output("Processing all available events")

# Either use the specific event IDs to retrieve data, or use the api_list
# if no specific event IDs are specified.
if event_ids:
listed_events = (
models.Event.stripe_class.retrieve(
id=event_id, api_key=djstripe_settings.STRIPE_SECRET_KEY
)
for event_id in event_ids
)
else:
list_kwargs = {}
if failed:
list_kwargs["delivery_success"] = False

if type_filter:
list_kwargs["type"] = type_filter

listed_events = models.Event.api_list(**list_kwargs)

self.process_events(listed_events)

def process_events(self, listed_events):
# Process each listed event. Capture failures and continue,
# outputting debug information as verbosity dictates.
count = 0
total = 0
for event_data in listed_events:
try:
total += 1
event = models.Event.process(data=event_data)
count += 1
self.verbose_output(" Synced Event {id}".format(id=event.id))
except Exception as exception:
self.verbose_output(
" Failed processing Event {id}".format(id=event_data["id"])
)
self.output(" {exception}".format(exception=exception))
self.verbose_traceback()

if total == 0:
self.output(" (no results)")
else:
self.output(
" Processed {count} out of {total} Events".format(
count=count, total=total
)
)
28 changes: 28 additions & 0 deletions djstripe/mixins.py
@@ -1,6 +1,8 @@
"""
dj-stripe mixins
"""
import sys
import traceback

from . import settings as djstripe_settings
from .models import Customer, Plan
Expand Down Expand Up @@ -33,3 +35,29 @@ def get_context_data(self, *args, **kwargs):
)
context["subscription"] = context["customer"].subscription
return context


class VerbosityAwareOutputMixin:
"""
A mixin class to provide verbosity aware output functions for management commands.
"""

def set_verbosity(self, options):
"""Set the verbosity based off the passed in options."""
self.verbosity = options["verbosity"]

def output(self, arg):
"""Print if output is not silenced."""
if self.verbosity > 0:
print(arg)

def verbose_output(self, arg):
"""Print only if output is verbose."""
if self.verbosity > 1:
print(arg)

def verbose_traceback(self):
"""Print out a traceback if the output is verbose."""
if self.verbosity > 1:
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_traceback)
18 changes: 18 additions & 0 deletions docs/usage/manually_syncing_with_stripe.rst
Expand Up @@ -25,6 +25,24 @@ With no arguments this will sync all supported models, or a list of models to sy
Note that this may be redundant since we recursively sync related objects.


You can manually reprocess events using the management commands ``djstripe_process_events``.
By default this processes all events, but options can be passed to limit the events processed.
Note the Stripe API documents a limitation where events are only guaranteed to be available for
30 days.

.. code-block::
# all events
./manage.py djstripe_process_events
# failed events (events with pending webhooks or where all webhook delivery attempts failed)
./manage.py djstripe_process_events --failed
# filter by event type (all payment_intent events in this example)
./manage.py djstripe_process_events --type payment_intent.*
# specific events by ID
./manage.py djstripe_process_events --ids evt_foo evt_bar
# more output for debugging processing failures
./manage.py djstripe_process_events -v 2
In Code
-------

Expand Down

0 comments on commit e8ae93c

Please sign in to comment.