Skip to content

Commit 9ca640b

Browse files
author
Ben Creech
committed
Fix pre-auth behavior
Before this change we were erroneously marking pre-auth'd charges as status=pending, when they're actually status=succeeded. We were (accidentally) working around this incorrect behavior in pre-auth'd PaymentIntents. To get this right we have to actually split the _trigger_payment method into two: a check for payment authorization (which we do on construction even for Charges created with capture=false), and a separate routine to actually capture the charge (which we do on construction for non-pre-auth'd charges, and on _api_capture for pre-auth'd charges). We then adjust the PaymentIntent wrapper to fit. This also fixes a tiny mistake in the Charge refund test; it was asserting the wrong variable.
1 parent c0f2e16 commit 9ca640b

File tree

2 files changed

+123
-77
lines changed

2 files changed

+123
-77
lines changed

localstripe/resources.py

+78-67
Original file line numberDiff line numberDiff line change
@@ -506,48 +506,41 @@ def __init__(self, amount=None, currency=None, description=None,
506506
self.status = 'pending'
507507
self.receipt_email = None
508508
self.receipt_number = None
509+
self.payment_intent = None
509510
self.payment_method = source.id
510511
self.statement_descriptor = statement_descriptor
511512
self.failure_code = None
512513
self.failure_message = None
513514
self.captured = capture
514515
self.balance_transaction = None
515516

516-
def _trigger_payment(self, on_success=None, on_failure_now=None,
517-
on_failure_later=None):
517+
def _is_async_payment_method(self):
518518
pm = PaymentMethod._api_retrieve(self.payment_method)
519-
async_payment = pm.type == 'sepa_debit'
520-
521-
if async_payment:
522-
if not self._authorized:
523-
async def callback():
524-
await asyncio.sleep(0.5)
525-
self.status = 'failed'
526-
if on_failure_later:
527-
on_failure_later()
528-
else:
529-
async def callback():
530-
await asyncio.sleep(0.5)
531-
txn = BalanceTransaction(amount=self.amount,
532-
currency=self.currency,
533-
description=self.description,
534-
exchange_rate=1.0,
535-
reporting_category='charge',
536-
source=self.id, type='charge')
537-
self.balance_transaction = txn.id
538-
self.status = 'succeeded'
539-
if on_success:
540-
on_success()
541-
asyncio.ensure_future(callback())
519+
return pm.type == 'sepa_debit'
542520

543-
else:
544-
if not self._authorized:
521+
def _handle_auth_failure(self, on_failure_now=None, on_failure_later=None):
522+
if self._is_async_payment_method():
523+
async def callback():
524+
await asyncio.sleep(0.5)
545525
self.status = 'failed'
546-
self.failure_code = 'card_declined'
547-
self.failure_message = 'Your card was declined.'
548-
if on_failure_now:
549-
on_failure_now()
550-
else:
526+
if on_failure_later:
527+
on_failure_later()
528+
asyncio.ensure_future(callback())
529+
else:
530+
self.status = 'failed'
531+
self.failure_code = 'card_declined'
532+
self.failure_message = 'Your card was declined.'
533+
if on_failure_now:
534+
on_failure_now()
535+
536+
raise UserError(402, 'Your card was declined.',
537+
{'code': 'card_declined', 'charge': self.id})
538+
539+
540+
def _trigger_payment(self, on_success=None):
541+
if self._is_async_payment_method():
542+
async def callback():
543+
await asyncio.sleep(0.5)
551544
txn = BalanceTransaction(amount=self.amount,
552545
currency=self.currency,
553546
description=self.description,
@@ -558,25 +551,39 @@ async def callback():
558551
self.status = 'succeeded'
559552
if on_success:
560553
on_success()
554+
asyncio.ensure_future(callback())
555+
556+
else:
557+
txn = BalanceTransaction(amount=self.amount,
558+
currency=self.currency,
559+
description=self.description,
560+
exchange_rate=1.0,
561+
reporting_category='charge',
562+
source=self.id, type='charge')
563+
self.balance_transaction = txn.id
564+
self.status = 'succeeded'
565+
if on_success:
566+
on_success()
561567

562568
@classmethod
563569
def _api_create(cls, **data):
564570
obj = super()._api_create(**data)
565571

566-
# for successful pre-auth, return unpaid charge
567-
if not obj.captured and obj._authorized:
568-
return obj
572+
obj._initialize_charge()
569573

570-
def on_failure():
571-
raise UserError(402, 'Your card was declined.',
572-
{'code': 'card_declined', 'charge': obj.id})
574+
return obj
573575

574-
obj._trigger_payment(
575-
on_failure_now=on_failure,
576-
on_failure_later=on_failure
577-
)
576+
def _initialize_charge(self, on_success=None, on_failure_now=None,
577+
on_failure_later=None):
578+
if not self._authorized:
579+
self._handle_auth_failure(on_failure_now=on_failure_now,
580+
on_failure_later=on_failure_later)
581+
return
578582

579-
return obj
583+
self.status = 'succeeded'
584+
585+
if self.captured:
586+
self._trigger_payment(on_success)
580587

581588
@classmethod
582589
def _api_capture(cls, id, amount=None, **kwargs):
@@ -592,24 +599,26 @@ def _api_capture(cls, id, amount=None, **kwargs):
592599
obj._capture(amount)
593600
return obj
594601

595-
def _capture(self, amount):
602+
def _capture(self, amount, on_success=None):
596603
if amount is None:
597604
amount = self.amount
598605

599606
amount = try_convert_to_int(amount)
600607
try:
601608
assert type(amount) is int and 0 <= amount <= self.amount
602-
assert self.captured is False
609+
assert self.captured is False and self.status == 'succeeded'
603610
except AssertionError:
604611
raise UserError(400, 'Bad request')
605612

606-
def on_success():
613+
def on_success_capture():
607614
self.captured = True
608615
if amount < self.amount:
609616
refunded = self.amount - amount
610617
Refund(charge=self.id, amount=refunded)
618+
if on_success:
619+
on_success()
611620

612-
self._trigger_payment(on_success)
621+
self._trigger_payment(on_success=on_success_capture)
613622

614623
@property
615624
def paid(self):
@@ -1106,7 +1115,6 @@ def _api_update(cls, id, **data):
11061115
def _api_delete(cls, id):
11071116
raise UserError(405, 'Method Not Allowed')
11081117

1109-
11101118
class Invoice(StripeObject):
11111119
object = 'invoice'
11121120
_id_prefix = 'in_'
@@ -1867,32 +1875,34 @@ def __init__(self, amount=None, currency=None, customer=None,
18671875
self._canceled = False
18681876
self._authentication_failed = False
18691877

1870-
def _trigger_payment(self):
1871-
if self.status != 'requires_confirmation':
1872-
raise UserError(400, 'Bad request')
1878+
def _on_success(self):
1879+
if self.invoice:
1880+
invoice = Invoice._api_retrieve(self.invoice)
1881+
invoice._on_payment_success()
18731882

1874-
def on_success():
1875-
if self.invoice:
1876-
invoice = Invoice._api_retrieve(self.invoice)
1877-
invoice._on_payment_success()
1883+
def _on_failure_now(self):
1884+
if self.invoice:
1885+
invoice = Invoice._api_retrieve(self.invoice)
1886+
invoice._on_payment_failure_now()
18781887

1879-
def on_failure_now():
1880-
if self.invoice:
1881-
invoice = Invoice._api_retrieve(self.invoice)
1882-
invoice._on_payment_failure_now()
1888+
def _on_failure_later(self):
1889+
if self.invoice:
1890+
invoice = Invoice._api_retrieve(self.invoice)
1891+
invoice._on_payment_failure_later()
18831892

1884-
def on_failure_later():
1885-
if self.invoice:
1886-
invoice = Invoice._api_retrieve(self.invoice)
1887-
invoice._on_payment_failure_later()
1893+
def _create_charge(self):
1894+
if self.status != 'requires_confirmation':
1895+
raise UserError(400, 'Bad request')
18881896

18891897
charge = Charge(amount=self.amount,
18901898
currency=self.currency,
18911899
customer=self.customer,
18921900
source=self.payment_method,
18931901
capture=(self.capture_method != "manual"))
1902+
charge.payment_intent = self.id
18941903
self.latest_charge = charge
1895-
charge._trigger_payment(on_success, on_failure_now, on_failure_later)
1904+
charge._initialize_charge(self._on_success, self._on_failure_now,
1905+
self._on_failure_later)
18961906

18971907
@property
18981908
def status(self):
@@ -1984,7 +1994,7 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):
19841994
'stripe_js': ''},
19851995
}
19861996
else:
1987-
obj._trigger_payment()
1997+
obj._create_charge()
19881998

19891999
return obj
19902000

@@ -2030,7 +2040,7 @@ def _api_authenticate(cls, id, client_secret=None, success=False,
20302040

20312041
obj.next_action = None
20322042
if success:
2033-
obj._trigger_payment()
2043+
obj._create_charge()
20342044
else:
20352045
obj._authentication_failed = True
20362046
obj.payment_method = None
@@ -2051,7 +2061,8 @@ def _api_capture(cls, id, amount_to_capture=None, **kwargs):
20512061
raise UserError(400, 'Bad request')
20522062

20532063
obj = cls._api_retrieve(id)
2054-
obj.latest_charge._capture(amount=amount_to_capture)
2064+
obj.latest_charge._capture(amount=amount_to_capture,
2065+
on_success=obj._on_success)
20552066
return obj
20562067

20572068

test.sh

+45-10
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ captured=$(
344344
refunded=$(
345345
curl -sSfg -u $SK: $HOST/v1/charges/$charge \
346346
| grep -oE '"amount_refunded": 200,')
347-
[ -n "$captured" ]
347+
[ -n "$refunded" ]
348348

349349
# create a pre-auth charge
350350
charge=$(
@@ -797,12 +797,22 @@ charge=$(
797797
-d capture=false \
798798
| grep -oE 'ch_\w+' | head -n 1)
799799

800-
# verify charge status pending
800+
# verify charge status succeeded.
801+
# pre-authed charges surprisingly show as status=succeeded with
802+
# charged=false.
803+
# (To see this in action, run the example charge creation from
804+
# https://docs.stripe.com/api/charges/create with -d capture=false,
805+
# and then GET .../v1/charges/$charge.)
801806
status=$(
802807
curl -sSfg -u $SK: $HOST/v1/charges/$charge \
803-
| grep -oE '"status": "pending"')
808+
| grep -oE '"status": "succeeded"')
804809
[ -n "$status" ]
805810

811+
not_captured=$(
812+
curl -sSfg -u $SK: $HOST/v1/charges/$charge \
813+
| grep -oE '"captured": false')
814+
[ -n "$not_captured" ]
815+
806816
# capture the charge
807817
curl -sSfg -u $SK: $HOST/v1/charges/$charge/capture \
808818
-X POST
@@ -814,7 +824,7 @@ status=$(
814824
[ -n "$status" ]
815825

816826
# create a non-chargeable source
817-
card=$(
827+
bad_card=$(
818828
curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \
819829
-d source[object]=card \
820830
-d source[number]=4000000000000341 \
@@ -827,15 +837,15 @@ card=$(
827837
code=$(
828838
curl -sg -o /dev/null -w "%{http_code}" \
829839
-u $SK: $HOST/v1/charges \
830-
-d source=$card \
840+
-d source=$bad_card \
831841
-d amount=1000 \
832842
-d currency=usd)
833843
[ "$code" = 402 ]
834844

835845
# create a normal charge
836846
charge=$(
837847
curl -sg -u $SK: $HOST/v1/charges \
838-
-d source=$card \
848+
-d source=$bad_card \
839849
-d amount=1000 \
840850
-d currency=usd \
841851
| grep -oE 'ch_\w+' | head -n 1)
@@ -846,12 +856,11 @@ status=$(
846856
| grep -oE '"status": "failed"')
847857
[ -n "$status" ]
848858

849-
850859
# create a pre-auth charge, observe 402 response
851860
code=$(
852861
curl -sg -o /dev/null -w "%{http_code}" \
853862
-u $SK: $HOST/v1/charges \
854-
-d source=$card \
863+
-d source=$bad_card \
855864
-d amount=1000 \
856865
-d currency=usd \
857866
-d capture=false)
@@ -860,7 +869,7 @@ code=$(
860869
# create a pre-auth charge
861870
charge=$(
862871
curl -sg -u $SK: $HOST/v1/charges \
863-
-d source=$card \
872+
-d source=$bad_card \
864873
-d amount=1000 \
865874
-d currency=usd \
866875
-d capture=false \
@@ -1083,7 +1092,7 @@ captured=$(
10831092
refunded=$(
10841093
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
10851094
| grep -oE '"amount_refunded": 200,')
1086-
[ -n "$captured" ]
1095+
[ -n "$refunded" ]
10871096

10881097
# create a pre-auth payment_intent
10891098
payment_intent=$(
@@ -1129,3 +1138,29 @@ refunded=$(
11291138
curl -sSfg -u $SK: $HOST/v1/payment_intents/$payment_intent \
11301139
| grep -oE '"amount_refunded": 1000,')
11311140
[ -n "$refunded" ]
1141+
1142+
# Create a payment intent on a bad card:
1143+
code=$(
1144+
curl -sg -u $SK: $HOST/v1/payment_intents -o /dev/null -w "%{http_code}" \
1145+
-d customer=$cus \
1146+
-d payment_method=$bad_card \
1147+
-d amount=1000 \
1148+
-d confirm=true \
1149+
-d currency=usd)
1150+
[ "$code" = 402 ]
1151+
1152+
# Once more with a delayed confirm:
1153+
payment_intent=$(
1154+
curl -sSfg -u $SK: $HOST/v1/payment_intents \
1155+
-d customer=$cus \
1156+
-d amount=1000 \
1157+
-d confirm=false \
1158+
-d payment_method=$bad_card \
1159+
-d currency=usd \
1160+
| grep -oE 'pi_\w+' | head -n 1)
1161+
1162+
# now run the confirm; it fails because the card is bad:
1163+
code=$(
1164+
curl -sg -u $SK: $HOST/v1/payment_intents/$payment_intent/confirm \
1165+
-X POST -o /dev/null -w "%{http_code}")
1166+
[ "$code" = 402 ]

0 commit comments

Comments
 (0)