Skip to content

Commit 9874e9b

Browse files
author
Ben Creech
committed
Add event querying and payment_intent events
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, and in localstripe is pending for 0.5 seconds).
1 parent caa0e94 commit 9874e9b

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

localstripe/resources.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,47 @@ def _api_update(cls, id, **data):
11151115
def _api_delete(cls, id):
11161116
raise UserError(405, 'Method Not Allowed')
11171117

1118+
@classmethod
1119+
def _api_list_all(cls, url, type=None, created=None, limit=None,
1120+
starting_after=None, **kwargs):
1121+
if kwargs:
1122+
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))
1123+
1124+
filters = []
1125+
try:
1126+
if type is not None:
1127+
assert _type(type) is str
1128+
filters.append(lambda obj: obj.type == type)
1129+
if created is not None:
1130+
assert _type(created) is dict
1131+
gt = try_convert_to_int(created.pop('gt', None))
1132+
if gt is not None:
1133+
filters.append(lambda obj: obj.created > gt)
1134+
1135+
gte = try_convert_to_int(created.pop('gte', None))
1136+
if gte is not None:
1137+
filters.append(lambda obj: obj.created >= gte)
1138+
1139+
lt = try_convert_to_int(created.pop('lt', None))
1140+
if lt is not None:
1141+
filters.append(lambda obj: obj.created < lt)
1142+
1143+
lte = try_convert_to_int(created.pop('lte', None))
1144+
if lte is not None:
1145+
filters.append(lambda obj: obj.created <= lte)
1146+
1147+
assert not created # no other params are supported
1148+
except AssertionError:
1149+
raise UserError(400, 'Bad request')
1150+
1151+
li = super()._api_list_all(
1152+
url, limit=limit, starting_after=starting_after
1153+
)
1154+
1155+
li._list = [obj for obj in li._list if all(f(obj) for f in filters)]
1156+
1157+
return li
1158+
11181159

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

18791920
def _on_success(self):
1921+
schedule_webhook(Event('payment_intent.succeeded', self))
18801922
if self.invoice:
18811923
invoice = Invoice._api_retrieve(self.invoice)
18821924
invoice._on_payment_success()
18831925

18841926
def _report_failure(self):
1927+
schedule_webhook(Event('payment_intent.payment_failed', self))
18851928
if self.invoice:
18861929
invoice = Invoice._api_retrieve(self.invoice)
18871930
invoice._on_payment_failure_now()
18881931

18891932
self.latest_charge._raise_failure()
18901933

1891-
def _on_failure_later(self):
1934+
def _report_async_failure(self):
1935+
schedule_webhook(Event('payment_intent.payment_failed', self))
18921936
if self.invoice:
18931937
invoice = Invoice._api_retrieve(self.invoice)
18941938
invoice._on_payment_failure_later()
@@ -1905,7 +1949,7 @@ def _create_charge(self, on_failure_now):
19051949
charge.payment_intent = self.id
19061950
self.latest_charge = charge
19071951
charge._initialize_charge(self._on_success, on_failure_now,
1908-
self._on_failure_later)
1952+
self._report_async_failure)
19091953

19101954
@property
19111955
def status(self):

test.sh

+43
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,12 @@ payment_intent=$(
10681068
-d capture_method=manual \
10691069
| grep -oE 'pi_\w+' | head -n 1)
10701070

1071+
# we don't get a payment_intent.succeeded event from the pre-auth:
1072+
succeded_event=$(
1073+
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.succeeded" \
1074+
| grep -oE "^ \"id\": \"$payment_intent\"" || true)
1075+
[ -z "$succeded_event" ]
1076+
10711077
# payment_intent was not captured
10721078
captured=$(
10731079
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
@@ -1088,6 +1094,12 @@ captured=$(
10881094
| grep -oE '"status": "succeeded"')
10891095
[ -n "$captured" ]
10901096

1097+
# we do get a payment_intent.succeeded event from the capture:
1098+
succeded_event=$(
1099+
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.succeeded" \
1100+
| grep -oE "^ \"id\": \"$payment_intent\"" || true)
1101+
[ -n "$succeded_event" ]
1102+
10911103
# difference between pre-auth and capture is refunded
10921104
refunded=$(
10931105
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
@@ -1165,6 +1177,37 @@ code=$(
11651177
-X POST -o /dev/null -w "%{http_code}")
11661178
[ "$code" = 402 ]
11671179

1180+
# we get a payment_intent.payment_failed event:
1181+
failed_event=$(
1182+
curl -sSfg -u $SK: "$HOST/v1/events?type=payment_intent.payment_failed" \
1183+
| grep -oE "^ \"id\": \"$payment_intent\"" || true)
1184+
[ -n "$failed_event" ]
1185+
1186+
# we don't get a payment_intent.succeeded event:
1187+
cus=$(curl -sSfg -u $SK: $HOST/v1/customers \
1188+
-d email=james.robinson@example.com \
1189+
| grep -oE 'cus_\w+' | head -n 1)
1190+
1191+
## test event timestamp filtering:
1192+
first_created=$(
1193+
curl -sSfg -u $SK: "$HOST/v1/events" \
1194+
| grep -oP -m 1 'created": \K([0-9]+)' || true)
1195+
[ -n "$first_created" ]
1196+
1197+
total_count=$(curl -sSfg -u $SK: $HOST/v1/events \
1198+
| grep -oP '^ "total_count": \K([0-9]+)')
1199+
[ "$total_count" -gt 1 ]
1200+
1201+
count=$(
1202+
curl -sSfg -u $SK: "$HOST/v1/events?created[lte]=$first_created" \
1203+
| grep -oP '^ "total_count": \K([0-9]+)')
1204+
[ "$count" -le "$total_count" ]
1205+
1206+
count=$(
1207+
curl -sSfg -u $SK: "$HOST/v1/events?created[lte]=$first_created&created[gt]=9999999999" \
1208+
| grep -oP '^ "total_count": \K([0-9]+)')
1209+
[ "$count" -eq 0 ]
1210+
11681211
# Create a customer with card 4000000000000341 (that fails upon payment) and
11691212
# make sure creating the subscription doesn't fail (although it creates it with
11701213
# status 'incomplete'). This how Stripe behaves, see

0 commit comments

Comments
 (0)