diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py index f0afff4cebe..6c1b404b91f 100644 --- a/addons/payment_stripe/controllers/main.py +++ b/addons/payment_stripe/controllers/main.py @@ -118,7 +118,8 @@ class StripeController(http.Controller): _logger.exception("unable to handle the event data; skipping to acknowledge") return '' - def _include_payment_intent_in_feedback_data(self, payment_intent, data): + @staticmethod + def _include_payment_intent_in_feedback_data(payment_intent, data): data.update({'payment_intent': payment_intent}) if payment_intent.get('charges', {}).get('total_count', 0) > 0: charge = payment_intent['charges']['data'][0] # Use the latest charge object @@ -127,7 +128,8 @@ class StripeController(http.Controller): 'payment_method': charge.get('payment_method_details'), }) - def _include_setup_intent_in_feedback_data(self, setup_intent, data): + @staticmethod + def _include_setup_intent_in_feedback_data(setup_intent, data): data.update({ 'setup_intent': setup_intent, 'payment_method': setup_intent.get('payment_method') diff --git a/addons/payment_stripe/models/payment_acquirer.py b/addons/payment_stripe/models/payment_acquirer.py index 8940e113624..a35fdcdd463 100644 --- a/addons/payment_stripe/models/payment_acquirer.py +++ b/addons/payment_stripe/models/payment_acquirer.py @@ -39,7 +39,7 @@ class PaymentAcquirer(models.Model): return 1.0 - def _stripe_make_request(self, endpoint, payload=None, method='POST'): + def _stripe_make_request(self, endpoint, payload=None, method='POST', offline=False): """ Make a request to Stripe API at the specified endpoint. Note: self.ensure_one() @@ -47,6 +47,7 @@ class PaymentAcquirer(models.Model): :param str endpoint: The endpoint to be reached by the request :param dict payload: The payload of the request :param str method: The HTTP method of the request + :param bool offline: Whether the operation of the transaction being processed is 'offline' :return The JSON-formatted content of the response :rtype: dict :raise: ValidationError if an HTTP error occurs @@ -63,7 +64,9 @@ class PaymentAcquirer(models.Model): # Stripe can send 4XX errors for payment failures (not only for badly-formed requests). # Check if an error code is present in the response content and raise only if not. # See https://stripe.com/docs/error-codes. + # If the request originates from an offline operation, don't raise and return the resp. if not response.ok \ + and not offline \ and 400 <= response.status_code < 500 \ and response.json().get('error'): # The 'code' entry is sometimes missing try: diff --git a/addons/payment_stripe/models/payment_transaction.py b/addons/payment_stripe/models/payment_transaction.py index f87fc2d7f1c..c384b59e1f7 100644 --- a/addons/payment_stripe/models/payment_transaction.py +++ b/addons/payment_stripe/models/payment_transaction.py @@ -165,31 +165,45 @@ class PaymentTransaction(models.Model): raise UserError("Stripe: " + _("The transaction is not linked to a token.")) payment_intent = self._stripe_create_payment_intent() - feedback_data = { - 'reference': self.reference, - 'payment_intent': payment_intent, - } + feedback_data = {'reference': self.reference} + StripeController._include_payment_intent_in_feedback_data(payment_intent, feedback_data) _logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data)) self._handle_feedback_data('stripe', feedback_data) def _stripe_create_payment_intent(self): """ Create and return a PaymentIntent. + Note: self.ensure_one() + :return: The Payment Intent :rtype: dict """ if not self.token_id.stripe_payment_method: # Pre-SCA token -> migrate it self.token_id._stripe_sca_migrate_customer() - payment_intent = self.acquirer_id._stripe_make_request('payment_intents', payload={ - 'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id), - 'currency': self.currency_id.name.lower(), - 'confirm': True, - 'customer': self.token_id.acquirer_ref, - 'off_session': True, - 'payment_method': self.token_id.stripe_payment_method, - 'description': self.reference, - }) + response = self.acquirer_id._stripe_make_request( + 'payment_intents', + payload={ + 'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id), + 'currency': self.currency_id.name.lower(), + 'confirm': True, + 'customer': self.token_id.acquirer_ref, + 'off_session': True, + 'payment_method': self.token_id.stripe_payment_method, + 'description': self.reference, + }, + offline=self.operation == 'offline', + ) + if 'error' not in response: + payment_intent = response + else: # A processing error was returned in place of the payment intent + error_msg = response['error'].get('message') + self._set_error("Stripe: " + _( + "The communication with the API failed.\n" + "Stripe gave us the following info about the problem:\n'%s'", error_msg + )) # Flag transaction as in error now as the intent status might have a valid value + payment_intent = response['error'].get('payment_intent') # Get the PI from the error + return payment_intent @api.model @@ -234,11 +248,14 @@ class PaymentTransaction(models.Model): if self.provider != 'stripe': return + if 'charge' in data: + self.acquirer_reference = data['charge']['id'] + # Handle the intent status - if self.operation == 'online_redirect' or self.operation == 'online_token': - intent_status = data.get('payment_intent', {}).get('status') - else: # 'validation' + if self.operation == 'validation': intent_status = data.get('setup_intent', {}).get('status') + else: # 'online_redirect', 'online_token', 'offline' + intent_status = data.get('payment_intent', {}).get('status') if not intent_status: raise ValidationError( "Stripe: " + _("Received data with missing intent status.")