Skip to content

Commit

Permalink
Add event querying and payment_intent events
Browse files Browse the repository at this point in the history
These features are technically separate but it's easier to test them together.

The Stripe event query API lets us filter by event type and timestamp.

Meanwhile payment_intent successes and failures generate events (this is
particularly important for sepa_direct_debit which on the real Stripe API
is typically pending for days, on the Stripe test API is typically pending
for a minute or so, and in localstripe is pending for 0.5 seconds).
  • Loading branch information
Ben Creech authored and adrienverge committed Nov 14, 2024
1 parent 6b0ff20 commit a8a79b7
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 2 deletions.
48 changes: 46 additions & 2 deletions localstripe/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,47 @@ def _api_update(cls, id, **data):
def _api_delete(cls, id):
raise UserError(405, 'Method Not Allowed')

@classmethod
def _api_list_all(cls, url, type=None, created=None, limit=None,
starting_after=None, **kwargs):
if kwargs:
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))

filters = []
try:
if type is not None:
assert _type(type) is str
filters.append(lambda obj: obj.type == type)
if created is not None:
assert _type(created) is dict
gt = try_convert_to_int(created.pop('gt', None))
if gt is not None:
filters.append(lambda obj: obj.created > gt)

gte = try_convert_to_int(created.pop('gte', None))
if gte is not None:
filters.append(lambda obj: obj.created >= gte)

lt = try_convert_to_int(created.pop('lt', None))
if lt is not None:
filters.append(lambda obj: obj.created < lt)

lte = try_convert_to_int(created.pop('lte', None))
if lte is not None:
filters.append(lambda obj: obj.created <= lte)

assert not created # no other params are supported
except AssertionError:
raise UserError(400, 'Bad request')

li = super()._api_list_all(
url, limit=limit, starting_after=starting_after
)

li._list = [obj for obj in li._list if all(f(obj) for f in filters)]

return li


class Invoice(StripeObject):
object = 'invoice'
Expand Down Expand Up @@ -1877,18 +1918,21 @@ def __init__(self, amount=None, currency=None, customer=None,
self._authentication_failed = False

def _on_success(self):
schedule_webhook(Event('payment_intent.succeeded', self))
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_success()

def _report_failure(self):
schedule_webhook(Event('payment_intent.payment_failed', self))
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_now()

self.latest_charge._raise_failure()

def _on_failure_later(self):
def _report_async_failure(self):
schedule_webhook(Event('payment_intent.payment_failed', self))
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_later()
Expand All @@ -1905,7 +1949,7 @@ def _create_charge(self, on_failure_now):
charge.payment_intent = self.id
self.latest_charge = charge
charge._initialize_charge(self._on_success, on_failure_now,
self._on_failure_later)
self._report_async_failure)

@property
def status(self):
Expand Down
44 changes: 44 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,12 @@ payment_intent=$(
-d capture_method=manual \
| grep -oE 'pi_\w+' | head -n 1)

# we don't get a payment_intent.succeeded event from the pre-auth:
succeeded_event=$(
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.succeeded" \
| grep -oE "\"id\": \"$payment_intent\"" || true)
[ -z "$succeeded_event" ]

# payment_intent was not captured
captured=$(
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
Expand All @@ -1096,6 +1102,12 @@ captured=$(
| grep -oE '"status": "succeeded"')
[ -n "$captured" ]

# we do get a payment_intent.succeeded event from the capture:
succeeded_event=$(
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.succeeded" \
| grep -oE "\"id\": \"$payment_intent\"")
[ -n "$succeeded_event" ]

# difference between pre-auth and capture is refunded
refunded=$(
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
Expand Down Expand Up @@ -1173,6 +1185,38 @@ code=$(
-X POST -o /dev/null -w "%{http_code}")
[ "$code" = 402 ]

# we get a payment_intent.payment_failed event:
failed_event=$(
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.payment_failed" \
| grep -oE "\"id\": \"$payment_intent\"")
[ -n "$failed_event" ]

# we don't get a payment_intent.succeeded event:
succeeded_event=$(
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.succeeded" \
| grep -oE "\"id\": \"$payment_intent\"" || true)
[ -z "$succeeded_event" ]

## test event timestamp filtering:
first_created=$(
curl -sSfg -u $SK: "$HOST/v1/events" \
| grep -oP -m 1 'created": \K([0-9]+)' || true)
[ -n "$first_created" ]

total_count=$(curl -sSfg -u $SK: $HOST/v1/events \
| grep -oP '^ "total_count": \K([0-9]+)')
[ "$total_count" -gt 1 ]

count=$(
curl -sSfg -u $SK: "$HOST/v1/events?created[lte]=$first_created" \
| grep -oP '^ "total_count": \K([0-9]+)')
[ "$count" -le "$total_count" ]

count=$(
curl -sSfg -u $SK: "$HOST/v1/events?created[lte]=$first_created&created[gt]=9999999999" \
| grep -oP '^ "total_count": \K([0-9]+)')
[ "$count" -eq 0 ]

# Create a customer with card 4000000000000341 (that fails upon payment) and
# make sure creating the subscription doesn't fail (although it creates it with
# status 'incomplete'). This how Stripe behaves, see
Expand Down

0 comments on commit a8a79b7

Please sign in to comment.