Skip to content

Commit

Permalink
Fix pre-auth behavior
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Ben Creech committed Oct 12, 2024
1 parent 8bd29d4 commit 6c99bee
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 84 deletions.
165 changes: 91 additions & 74 deletions localstripe/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,48 +506,22 @@ def __init__(self, amount=None, currency=None, description=None,
self.status = 'pending'
self.receipt_email = None
self.receipt_number = None
self.payment_intent = None
self.payment_method = source.id
self.statement_descriptor = statement_descriptor
self.failure_code = None
self.failure_message = None
self.captured = capture
self.balance_transaction = None

def _trigger_payment(self, on_success=None, on_failure_now=None,
on_failure_later=None):
def _is_async_payment_method(self):
pm = PaymentMethod._api_retrieve(self.payment_method)
async_payment = pm.type == 'sepa_debit'
return pm.type == 'sepa_debit'

if async_payment:
if not self._authorized:
async def callback():
await asyncio.sleep(0.5)
self.status = 'failed'
if on_failure_later:
on_failure_later()
else:
async def callback():
await asyncio.sleep(0.5)
txn = BalanceTransaction(amount=self.amount,
currency=self.currency,
description=self.description,
exchange_rate=1.0,
reporting_category='charge',
source=self.id, type='charge')
self.balance_transaction = txn.id
self.status = 'succeeded'
if on_success:
on_success()
asyncio.ensure_future(callback())

else:
if not self._authorized:
self.status = 'failed'
self.failure_code = 'card_declined'
self.failure_message = 'Your card was declined.'
if on_failure_now:
on_failure_now()
else:
def _trigger_payment(self, on_success=None):
if self._is_async_payment_method():
async def callback():
await asyncio.sleep(0.5)
txn = BalanceTransaction(amount=self.amount,
currency=self.currency,
description=self.description,
Expand All @@ -558,25 +532,58 @@ async def callback():
self.status = 'succeeded'
if on_success:
on_success()
asyncio.ensure_future(callback())

else:
txn = BalanceTransaction(amount=self.amount,
currency=self.currency,
description=self.description,
exchange_rate=1.0,
reporting_category='charge',
source=self.id, type='charge')
self.balance_transaction = txn.id
self.status = 'succeeded'
if on_success:
on_success()

@classmethod
def _api_create(cls, **data):
obj = super()._api_create(**data)

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

def on_failure():
raise UserError(402, 'Your card was declined.',
{'code': 'card_declined', 'charge': obj.id})
return obj

obj._trigger_payment(
on_failure_now=on_failure,
on_failure_later=on_failure
)
def _set_auth_failure(self):
self.status = 'failed'
self.failure_code = 'card_declined'
self.failure_message = 'Your card was declined.'

return obj
def _raise_failure(self):
raise UserError(402, self.failure_message,
{'code': self.failure_code, 'charge': self.id})

def _initialize_charge(self, on_success=None, on_failure_now=None,
on_failure_later=None):
if not self._authorized:
if self._is_async_payment_method():
async def callback():
await asyncio.sleep(0.5)
self._set_auth_failure()
if on_failure_later:
on_failure_later()
asyncio.ensure_future(callback())
else:
self._set_auth_failure()
if on_failure_now:
on_failure_now()

return

self.status = 'succeeded'

if self.captured:
self._trigger_payment(on_success)

@classmethod
def _api_capture(cls, id, amount=None, **kwargs):
Expand All @@ -592,24 +599,26 @@ def _api_capture(cls, id, amount=None, **kwargs):
obj._capture(amount)
return obj

def _capture(self, amount):
def _capture(self, amount, on_success=None):
if amount is None:
amount = self.amount

amount = try_convert_to_int(amount)
try:
assert type(amount) is int and 0 <= amount <= self.amount
assert self.captured is False
assert self.captured is False and self.status == 'succeeded'
except AssertionError:
raise UserError(400, 'Bad request')

def on_success():
def on_success_capture():
self.captured = True
if amount < self.amount:
refunded = self.amount - amount
Refund(charge=self.id, amount=refunded)
if on_success:
on_success()

self._trigger_payment(on_success)
self._trigger_payment(on_success=on_success_capture)

@property
def paid(self):
Expand Down Expand Up @@ -1573,7 +1582,7 @@ def _api_pay_invoice(cls, id):
payment_method=pm.id)
obj.payment_intent = pi.id
pi.invoice = obj.id
PaymentIntent._api_confirm(obj.payment_intent)
pi._confirm(on_failure_now=lambda: None)

return obj

Expand Down Expand Up @@ -1867,32 +1876,36 @@ def __init__(self, amount=None, currency=None, customer=None,
self._canceled = False
self._authentication_failed = False

def _trigger_payment(self):
if self.status != 'requires_confirmation':
raise UserError(400, 'Bad request')
def _on_success(self):
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_success()

def on_success():
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_success()
def _report_failure(self):
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_now()

def on_failure_now():
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_now()
self.latest_charge._raise_failure()

def on_failure_later():
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_later()
def _on_failure_later(self):
if self.invoice:
invoice = Invoice._api_retrieve(self.invoice)
invoice._on_payment_failure_later()

def _create_charge(self, on_failure_now):
if self.status != 'requires_confirmation':
raise UserError(400, 'Bad request')

charge = Charge(amount=self.amount,
currency=self.currency,
customer=self.customer,
source=self.payment_method,
capture=(self.capture_method != "manual"))
charge.payment_intent = self.id
self.latest_charge = charge
charge._trigger_payment(on_success, on_failure_now, on_failure_later)
charge._initialize_charge(self._on_success, on_failure_now,
self._on_failure_later)

@property
def status(self):
Expand Down Expand Up @@ -1953,7 +1966,7 @@ def _api_create(cls, confirm=None, off_session=None, **data):
obj = super()._api_create(**data)

if confirm:
cls._api_confirm(obj.id)
obj._confirm(on_failure_now=obj._report_failure)

return obj

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

obj._authentication_failed = False
payment_method = PaymentMethod._api_retrieve(obj.payment_method)
obj._confirm(on_failure_now=obj._report_failure)

return obj

def _confirm(self, on_failure_now):
self._authentication_failed = False
payment_method = PaymentMethod._api_retrieve(self.payment_method)
if payment_method._requires_authentication():
obj.next_action = {
self.next_action = {
'type': 'use_stripe_sdk',
'use_stripe_sdk': {'type': 'three_d_secure_redirect',
'stripe_js': ''},
}
else:
obj._trigger_payment()

return obj
self._create_charge(on_failure_now=on_failure_now)

@classmethod
def _api_cancel(cls, id, **kwargs):
Expand Down Expand Up @@ -2030,7 +2046,7 @@ def _api_authenticate(cls, id, client_secret=None, success=False,

obj.next_action = None
if success:
obj._trigger_payment()
obj._create_charge(on_failure_now=obj._report_failure)
else:
obj._authentication_failed = True
obj.payment_method = None
Expand All @@ -2051,7 +2067,8 @@ def _api_capture(cls, id, amount_to_capture=None, **kwargs):
raise UserError(400, 'Bad request')

obj = cls._api_retrieve(id)
obj.latest_charge._capture(amount=amount_to_capture)
obj.latest_charge._capture(amount=amount_to_capture,
on_success=obj._on_success)
return obj


Expand Down
Loading

0 comments on commit 6c99bee

Please sign in to comment.