Skip to content

Commit 4e29f09

Browse files
Ben Creechadrienverge
Ben Creech
authored andcommitted
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 also split the PaymentIntent._api_confirm method into two for more control of error handling. 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 1ad01ad commit 4e29f09

File tree

2 files changed

+136
-84
lines changed

2 files changed

+136
-84
lines changed

localstripe/resources.py

+91-74
Original file line numberDiff line numberDiff line change
@@ -506,48 +506,22 @@ 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'
519+
return pm.type == 'sepa_debit'
520520

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())
542-
543-
else:
544-
if not self._authorized:
545-
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:
521+
def _trigger_payment(self, on_success=None):
522+
if self._is_async_payment_method():
523+
async def callback():
524+
await asyncio.sleep(0.5)
551525
txn = BalanceTransaction(amount=self.amount,
552526
currency=self.currency,
553527
description=self.description,
@@ -558,25 +532,58 @@ async def callback():
558532
self.status = 'succeeded'
559533
if on_success:
560534
on_success()
535+
asyncio.ensure_future(callback())
536+
537+
else:
538+
txn = BalanceTransaction(amount=self.amount,
539+
currency=self.currency,
540+
description=self.description,
541+
exchange_rate=1.0,
542+
reporting_category='charge',
543+
source=self.id, type='charge')
544+
self.balance_transaction = txn.id
545+
self.status = 'succeeded'
546+
if on_success:
547+
on_success()
561548

562549
@classmethod
563550
def _api_create(cls, **data):
564551
obj = super()._api_create(**data)
565552

566-
# for successful pre-auth, return unpaid charge
567-
if not obj.captured and obj._authorized:
568-
return obj
553+
obj._initialize_charge(on_failure_now=obj._raise_failure)
569554

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

574-
obj._trigger_payment(
575-
on_failure_now=on_failure,
576-
on_failure_later=on_failure
577-
)
557+
def _set_auth_failure(self):
558+
self.status = 'failed'
559+
self.failure_code = 'card_declined'
560+
self.failure_message = 'Your card was declined.'
578561

579-
return obj
562+
def _raise_failure(self):
563+
raise UserError(402, self.failure_message,
564+
{'code': self.failure_code, 'charge': self.id})
565+
566+
def _initialize_charge(self, on_success=None, on_failure_now=None,
567+
on_failure_later=None):
568+
if not self._authorized:
569+
if self._is_async_payment_method():
570+
async def callback():
571+
await asyncio.sleep(0.5)
572+
self._set_auth_failure()
573+
if on_failure_later:
574+
on_failure_later()
575+
asyncio.ensure_future(callback())
576+
else:
577+
self._set_auth_failure()
578+
if on_failure_now:
579+
on_failure_now()
580+
581+
return
582+
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):
@@ -1573,7 +1582,7 @@ def _api_pay_invoice(cls, id):
15731582
payment_method=pm.id)
15741583
obj.payment_intent = pi.id
15751584
pi.invoice = obj.id
1576-
PaymentIntent._api_confirm(obj.payment_intent)
1585+
pi._confirm(on_failure_now=lambda: None)
15771586

15781587
return obj
15791588

@@ -1867,32 +1876,36 @@ def __init__(self, amount=None, currency=None, customer=None,
18671876
self._canceled = False
18681877
self._authentication_failed = False
18691878

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

1874-
def on_success():
1875-
if self.invoice:
1876-
invoice = Invoice._api_retrieve(self.invoice)
1877-
invoice._on_payment_success()
1884+
def _report_failure(self):
1885+
if self.invoice:
1886+
invoice = Invoice._api_retrieve(self.invoice)
1887+
invoice._on_payment_failure_now()
18781888

1879-
def on_failure_now():
1880-
if self.invoice:
1881-
invoice = Invoice._api_retrieve(self.invoice)
1882-
invoice._on_payment_failure_now()
1889+
self.latest_charge._raise_failure()
18831890

1884-
def on_failure_later():
1885-
if self.invoice:
1886-
invoice = Invoice._api_retrieve(self.invoice)
1887-
invoice._on_payment_failure_later()
1891+
def _on_failure_later(self):
1892+
if self.invoice:
1893+
invoice = Invoice._api_retrieve(self.invoice)
1894+
invoice._on_payment_failure_later()
1895+
1896+
def _create_charge(self, on_failure_now):
1897+
if self.status != 'requires_confirmation':
1898+
raise UserError(400, 'Bad request')
18881899

18891900
charge = Charge(amount=self.amount,
18901901
currency=self.currency,
18911902
customer=self.customer,
18921903
source=self.payment_method,
18931904
capture=(self.capture_method != "manual"))
1905+
charge.payment_intent = self.id
18941906
self.latest_charge = charge
1895-
charge._trigger_payment(on_success, on_failure_now, on_failure_later)
1907+
charge._initialize_charge(self._on_success, on_failure_now,
1908+
self._on_failure_later)
18961909

18971910
@property
18981911
def status(self):
@@ -1953,7 +1966,7 @@ def _api_create(cls, confirm=None, off_session=None, **data):
19531966
obj = super()._api_create(**data)
19541967

19551968
if confirm:
1956-
cls._api_confirm(obj.id)
1969+
obj._confirm(on_failure_now=obj._report_failure)
19571970

19581971
return obj
19591972

@@ -1975,18 +1988,21 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):
19751988
if obj.status != 'requires_confirmation':
19761989
raise UserError(400, 'Bad request')
19771990

1978-
obj._authentication_failed = False
1979-
payment_method = PaymentMethod._api_retrieve(obj.payment_method)
1991+
obj._confirm(on_failure_now=obj._report_failure)
1992+
1993+
return obj
1994+
1995+
def _confirm(self, on_failure_now):
1996+
self._authentication_failed = False
1997+
payment_method = PaymentMethod._api_retrieve(self.payment_method)
19801998
if payment_method._requires_authentication():
1981-
obj.next_action = {
1999+
self.next_action = {
19822000
'type': 'use_stripe_sdk',
19832001
'use_stripe_sdk': {'type': 'three_d_secure_redirect',
19842002
'stripe_js': ''},
19852003
}
19862004
else:
1987-
obj._trigger_payment()
1988-
1989-
return obj
2005+
self._create_charge(on_failure_now=on_failure_now)
19902006

19912007
@classmethod
19922008
def _api_cancel(cls, id, **kwargs):
@@ -2030,7 +2046,7 @@ def _api_authenticate(cls, id, client_secret=None, success=False,
20302046

20312047
obj.next_action = None
20322048
if success:
2033-
obj._trigger_payment()
2049+
obj._create_charge(on_failure_now=obj._report_failure)
20342050
else:
20352051
obj._authentication_failed = True
20362052
obj.payment_method = None
@@ -2051,7 +2067,8 @@ def _api_capture(cls, id, amount_to_capture=None, **kwargs):
20512067
raise UserError(400, 'Bad request')
20522068

20532069
obj = cls._api_retrieve(id)
2054-
obj.latest_charge._capture(amount=amount_to_capture)
2070+
obj.latest_charge._capture(amount=amount_to_capture,
2071+
on_success=obj._on_success)
20552072
return obj
20562073

20572074

0 commit comments

Comments
 (0)