From f4ca7290ac377a986e39d26d8a792c2a9291425c Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Wed, 2 Feb 2022 12:00:48 +0000 Subject: [PATCH] [IMP] payment(_*): search only once for the transaction Before this commit, most acquirers needed to run several successive searches for the transaction whose reference was received by a controller in notification data. This is because the security checks run on the notification data require access to the acquirer through the transaction record which was immediately discarded. Starting with this commit, all `*_feedback_data` method are no longer decorated with `api.model` and can use the transaction record they're called on if provided. They are also renamed to `*_notification_data`. task-2737144 closes odoo/odoo#83850 Related: odoo/enterprise#23938 Signed-off-by: Antoine Vandevenne (anv) --- addons/payment/models/payment_transaction.py | 35 +++++------ addons/payment_adyen/controllers/main.py | 32 +++++----- .../models/payment_transaction.py | 61 ++++++++++--------- addons/payment_adyen/tests/test_adyen.py | 21 +++---- addons/payment_alipay/controllers/main.py | 8 +-- .../models/payment_transaction.py | 23 ++++--- addons/payment_alipay/tests/test_alipay.py | 10 +-- addons/payment_authorize/controllers/main.py | 8 +-- .../models/payment_transaction.py | 49 +++++---------- addons/payment_buckaroo/controllers/main.py | 8 +-- .../models/payment_transaction.py | 21 +++---- .../payment_buckaroo/tests/test_buckaroo.py | 6 +- addons/payment_mollie/controllers/main.py | 8 +-- .../models/payment_transaction.py | 24 ++++---- addons/payment_ogone/controllers/main.py | 4 +- .../models/payment_transaction.py | 50 ++++++++------- addons/payment_ogone/tests/test_ogone.py | 2 +- addons/payment_paypal/controllers/main.py | 10 ++- .../models/payment_transaction.py | 25 ++++---- addons/payment_paypal/tests/test_paypal.py | 8 +-- addons/payment_payulatam/controllers/main.py | 2 +- .../models/payment_transaction.py | 27 ++++---- .../payment_payulatam/tests/test_payulatam.py | 8 ++- addons/payment_payumoney/controllers/main.py | 4 +- .../models/payment_transaction.py | 23 ++++--- addons/payment_sips/controllers/main.py | 8 +-- .../models/payment_transaction.py | 21 +++---- addons/payment_sips/tests/test_sips.py | 10 +-- addons/payment_stripe/controllers/main.py | 38 ++++++------ .../models/payment_transaction.py | 60 +++++++++--------- addons/payment_stripe/tests/test_stripe.py | 2 +- addons/payment_test/controllers/main.py | 4 +- .../models/payment_transaction.py | 25 ++++---- addons/payment_transfer/controllers/main.py | 2 +- .../models/payment_transaction.py | 17 +++--- 35 files changed, 319 insertions(+), 345 deletions(-) diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index f273f7648dc..33aecb65134 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -605,43 +605,44 @@ class PaymentTransaction(models.Model): **custom_create_values, }) - @api.model - def _handle_feedback_data(self, provider, data): - """ Match the transaction with the feedback data, update its state and return it. + def _handle_notification_data(self, provider, notification_data): + """ Match the transaction with the notification data, update its state and return it. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction :rtype: recordset of `payment.transaction` """ - tx = self._get_tx_from_feedback_data(provider, data) - tx._process_feedback_data(data) + tx = self._get_tx_from_notification_data(provider, notification_data) + tx._process_notification_data(notification_data) tx._execute_callback() return tx - @api.model - def _get_tx_from_feedback_data(self, provider, data): - """ Find the transaction based on the feedback data. + def _get_tx_from_notification_data(self, provider, notification_data): + """ Find the transaction based on the notification data. - For an acquirer to handle transaction post-processing, it must overwrite this method and - return the transaction matching the data. + For an acquirer to handle transaction processing, it must overwrite this method and return + the transaction matching the notification data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the acquirer + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` """ return self - def _process_feedback_data(self, data): - """ Update the transaction state and the acquirer reference based on the feedback data. + def _process_notification_data(self, notification_data): + """ Update the transaction state and the acquirer reference based on the notification data. - For an acquirer to handle transaction post-processing, it must overwrite this method and - process the feedback data. + This method should normally never be called directly. The correct method to call upon + receiving notification data is `_handle_notification_data`. + + For an acquirer to handle transaction processing, it must overwrite this method and process + the notification data. Note: self.ensure_one() - :param dict data: The feedback data sent by the acquirer + :param dict notification_data: The notification data sent by the provider :return: None """ self.ensure_one() diff --git a/addons/payment_adyen/controllers/main.py b/addons/payment_adyen/controllers/main.py index ca5504f00c6..45453591e58 100644 --- a/addons/payment_adyen/controllers/main.py +++ b/addons/payment_adyen/controllers/main.py @@ -85,7 +85,7 @@ class AdyenController(http.Controller): self, acquirer_id, reference, converted_amount, currency_id, partner_id, payment_method, access_token, browser_info=None ): - """ Make a payment request and process the feedback data. + """ Make a payment request and handle the notification data. :param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id :param str reference: The reference of the transaction @@ -147,14 +147,14 @@ class AdyenController(http.Controller): "payment request response for transaction with reference %s:\n%s", reference, pprint.pformat(response_content) ) - request.env['payment.transaction'].sudo()._handle_feedback_data( + tx_sudo._handle_notification_data( 'adyen', dict(response_content, merchantReference=reference), # Match the transaction ) return response_content @http.route('/payment/adyen/payment_details', type='json', auth='public') def adyen_payment_details(self, acquirer_id, reference, payment_details): - """ Submit the details of the additional actions and process the feedback data. + """ Submit the details of the additional actions and handle the notification data. The additional actions can have been performed both from the inline form or during a redirection. @@ -179,7 +179,7 @@ class AdyenController(http.Controller): "payment details request response for transaction with reference %s:\n%s", reference, pprint.pformat(response_content) ) - request.env['payment.transaction'].sudo()._handle_feedback_data( + request.env['payment.transaction'].sudo()._handle_notification_data( 'adyen', dict(response_content, merchantReference=reference), # Match the transaction ) @@ -187,7 +187,7 @@ class AdyenController(http.Controller): @http.route('/payment/adyen/return', type='http', auth='public', csrf=False, save_session=False) def adyen_return_from_3ds_auth(self, **data): - """ Process the feedback data sent by Adyen after redirection from 3DS1 authentication page. + """ Process the authentication data sent by Adyen after redirection from the 3DS1 page. The route is flagged with `save_session=False` to prevent Odoo from assigning a new session to the user if they are redirected to this route with a POST request. Indeed, as the session @@ -197,11 +197,11 @@ class AdyenController(http.Controller): will satisfy any specification of the `SameSite` attribute, the session of the user will be retrieved and with it the transaction which will be immediately post-processed. - :param dict data: Feedback data. May include custom params sent to Adyen in the request to - allow matching the transaction when redirected here. + :param dict data: The authentication result data. May include custom params sent to Adyen in + the request to allow matching the transaction when redirected here. """ # Retrieve the transaction based on the reference included in the return url - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'adyen', data ) @@ -248,12 +248,10 @@ class AdyenController(http.Controller): ) try: # Check the integrity of the notification - PaymentTransaction = request.env['payment.transaction'] - tx_sudo = PaymentTransaction.sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'adyen', notification_data ) - acquirer_sudo = tx_sudo.acquirer_id # Find the acquirer based on the transaction - self._verify_notification_signature(notification_data, acquirer_sudo.adyen_hmac_key) + self._verify_notification_signature(notification_data, tx_sudo) # Check whether the event of the notification succeeded and reshape the notification # data for parsing @@ -268,19 +266,20 @@ class AdyenController(http.Controller): else: continue # Don't handle unsupported event codes and failed events - # Handle the notification data as a regular feedback - PaymentTransaction.sudo()._handle_feedback_data('adyen', notification_data) + # Handle the notification data as if they were feedback of a S2S payment request + tx_sudo._handle_notification_data('adyen', notification_data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") return '[accepted]' # Acknowledge the notification @staticmethod - def _verify_notification_signature(notification_data, hmac_key): + def _verify_notification_signature(notification_data, tx_sudo): """ Check that the received signature matches the expected one. :param dict notification_data: The notification payload containing the received signature - :param str hmac_key: The HMAC key of the acquirer handling the transaction + :param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a + `payment.transaction` record :return: None :raise: :class:`werkzeug.exceptions.Forbidden` if the signatures don't match """ @@ -291,6 +290,7 @@ class AdyenController(http.Controller): raise Forbidden() # Compare the received signature with the expected signature computed from the payload + hmac_key = tx_sudo.acquirer_id.adyen_hmac_key expected_signature = AdyenController._compute_signature(notification_data, hmac_key) if not hmac.compare_digest(received_signature, expected_signature): _logger.warning("received notification with invalid signature") diff --git a/addons/payment_adyen/models/payment_transaction.py b/addons/payment_adyen/models/payment_transaction.py index 91a4da15737..78b6f3e15e9 100644 --- a/addons/payment_adyen/models/payment_transaction.py +++ b/addons/payment_adyen/models/payment_transaction.py @@ -88,7 +88,7 @@ class PaymentTransaction(models.Model): "payment request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(response_content) ) - self._handle_feedback_data('adyen', response_content) + self._handle_notification_data('adyen', response_content) def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True): """ Override of payment to send a refund request to Adyen. @@ -145,31 +145,30 @@ class PaymentTransaction(models.Model): return refund_tx - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Adyen data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'adyen': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'adyen' or len(tx) == 1: return tx - reference = data.get('merchantReference') + reference = notification_data.get('merchantReference') if not reference: raise ValidationError("Adyen: " + _("Received data with missing merchant reference")) - event_code = data.get('eventCode') + event_code = notification_data.get('eventCode') tx = self.search([('reference', '=', reference), ('provider', '=', 'adyen')]) if event_code == 'REFUND' and (not tx or tx.operation != 'refund'): # If a refund is initiated from Adyen, the merchant reference can be personalized. We # need to get the source transaction and manually create the refund transaction. - source_acquirer_reference = data.get('originalReference') + source_acquirer_reference = notification_data.get('originalReference') source_tx = self.search( [('acquirer_reference', '=', source_acquirer_reference), ('provider', '=', 'adyen')] ) @@ -177,7 +176,9 @@ class PaymentTransaction(models.Model): # Manually create a refund transaction with a new reference. The reference of # the refund transaction was personalized from Adyen and could be identical to # that of an existing transaction. - tx = self._adyen_create_refund_tx_from_feedback_data(source_tx, data) + tx = self._adyen_create_refund_tx_from_notification_data( + source_tx, notification_data + ) else: # The refund was initiated for an unknown source transaction pass # Don't do anything with the refund notification @@ -187,18 +188,18 @@ class PaymentTransaction(models.Model): ) return tx - def _adyen_create_refund_tx_from_feedback_data(self, source_tx, data): + def _adyen_create_refund_tx_from_notification_data(self, source_tx, notification_data): """ Create a refund transaction based on Adyen data. :param recordset source_tx: The source transaction for which a refund is initiated, as a `payment.transaction` recordset - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The created refund transaction :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received """ - refund_acquirer_reference = data.get('pspReference') - amount_to_refund = data.get('amount', {}).get('value') + refund_acquirer_reference = notification_data.get('pspReference') + amount_to_refund = notification_data.get('amount', {}).get('value') if not refund_acquirer_reference or not amount_to_refund: raise ValidationError( "Adyen: " + _("Received refund data with missing transaction values") @@ -211,35 +212,36 @@ class PaymentTransaction(models.Model): amount_to_refund=converted_amount, acquirer_reference=refund_acquirer_reference ) - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Adyen data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'adyen': return # Handle the acquirer reference - if 'pspReference' in data: - self.acquirer_reference = data.get('pspReference') + if 'pspReference' in notification_data: + self.acquirer_reference = notification_data.get('pspReference') # Handle the payment state - payment_state = data.get('resultCode') - refusal_reason = data.get('refusalReason') or data.get('reason') + payment_state = notification_data.get('resultCode') + refusal_reason = notification_data.get('refusalReason') or notification_data.get('reason') if not payment_state: raise ValidationError("Adyen: " + _("Received data with missing payment state.")) if payment_state in RESULT_CODES_MAPPING['pending']: self._set_pending() elif payment_state in RESULT_CODES_MAPPING['done']: - has_token_data = 'recurring.recurringDetailReference' in data.get('additionalData', {}) + additional_data = notification_data.get('additionalData', {}) + has_token_data = 'recurring.recurringDetailReference' in additional_data if self.tokenize and has_token_data: - self._adyen_tokenize_from_feedback_data(data) + self._adyen_tokenize_from_notification_data(notification_data) self._set_done() if self.operation == 'refund': self.env.ref('payment.cron_post_process_payment_tx')._trigger() @@ -272,22 +274,23 @@ class PaymentTransaction(models.Model): "Adyen: " + _("Received data with invalid payment state: %s", payment_state) ) - def _adyen_tokenize_from_feedback_data(self, data): - """ Create a new token based on the feedback data. + def _adyen_tokenize_from_notification_data(self, notification_data): + """ Create a new token based on the notification data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ self.ensure_one() + additional_data = notification_data['additionalData'] token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': payment_utils.build_token_name(data['additionalData'].get('cardSummary')), + 'name': payment_utils.build_token_name(additional_data.get('cardSummary')), 'partner_id': self.partner_id.id, - 'acquirer_ref': data['additionalData']['recurring.recurringDetailReference'], - 'adyen_shopper_reference': data['additionalData']['recurring.shopperReference'], + 'acquirer_ref': additional_data['recurring.recurringDetailReference'], + 'adyen_shopper_reference': additional_data['recurring.shopperReference'], 'verified': True, # The payment is authorized, so the payment method is valid }) self.write({ diff --git a/addons/payment_adyen/tests/test_adyen.py b/addons/payment_adyen/tests/test_adyen.py index f3ddf9bbe65..d83a5dcd546 100644 --- a/addons/payment_adyen/tests/test_adyen.py +++ b/addons/payment_adyen/tests/test_adyen.py @@ -94,41 +94,34 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon): '._verify_notification_signature' ) as signature_check_mock, patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_json_request(url, data=self.WEBHOOK_NOTIFICATION_BATCH_DATA) self.assertEqual(signature_check_mock.call_count, 1) def test_accept_webhook_notification_with_valid_signature(self): """ Test the verification of a webhook notification with a valid signature. """ + tx = self.create_transaction('direct') self._assert_does_not_raise( Forbidden, AdyenController._verify_notification_signature, self.WEBHOOK_NOTIFICATION_PAYLOAD, - self.acquirer.adyen_hmac_key, + tx, ) @mute_logger('odoo.addons.payment_adyen.controllers.main') def test_reject_webhook_notification_with_missing_signature(self): """ Test the verification of a webhook notification with a missing signature. """ payload = dict(self.WEBHOOK_NOTIFICATION_PAYLOAD, additionalData={'hmacSignature': None}) - self.assertRaises( - Forbidden, - AdyenController._verify_notification_signature, - payload, - self.acquirer.adyen_hmac_key, - ) + tx = self.create_transaction('direct') + self.assertRaises(Forbidden, AdyenController._verify_notification_signature, payload, tx) @mute_logger('odoo.addons.payment_adyen.controllers.main') def test_reject_webhook_notification_with_invalid_signature(self): """ Test the verification of a webhook notification with an invalid signature. """ payload = dict(self.WEBHOOK_NOTIFICATION_PAYLOAD, additionalData={'hmacSignature': 'dummy'}) - self.assertRaises( - Forbidden, - AdyenController._verify_notification_signature, - payload, - self.acquirer.adyen_hmac_key, - ) + tx = self.create_transaction('direct') + self.assertRaises(Forbidden, AdyenController._verify_notification_signature, payload, tx) def test_adyen_neutralize(self): self.env['payment.acquirer']._neutralize() diff --git a/addons/payment_alipay/controllers/main.py b/addons/payment_alipay/controllers/main.py index 2e4af63883a..246b154b9ca 100644 --- a/addons/payment_alipay/controllers/main.py +++ b/addons/payment_alipay/controllers/main.py @@ -29,13 +29,13 @@ class AlipayController(http.Controller): _logger.info("handling redirection from Alipay with data:\n%s", pprint.pformat(data)) # Check the integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'alipay', data ) self._verify_notification_signature(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('alipay', data) + tx_sudo._handle_notification_data('alipay', data) return request.redirect('/payment/status') @http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False) @@ -51,14 +51,14 @@ class AlipayController(http.Controller): _logger.info("notification received from Alipay with data:\n%s", pprint.pformat(data)) try: # Check the origin and integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'alipay', data ) self._verify_notification_origin(data, tx_sudo) self._verify_notification_signature(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('alipay', data) + tx_sudo._handle_notification_data('alipay', data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") diff --git a/addons/payment_alipay/models/payment_transaction.py b/addons/payment_alipay/models/payment_transaction.py index 827843ca336..29ac7d60375 100644 --- a/addons/payment_alipay/models/payment_transaction.py +++ b/addons/payment_alipay/models/payment_transaction.py @@ -68,23 +68,22 @@ class PaymentTransaction(models.Model): }) return rendering_values - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Alipay data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'alipay': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'alipay' or len(tx) == 1: return tx - reference = data.get('reference') or data.get('out_trade_no') - txn_id = data.get('trade_no') + reference = notification_data.get('reference') or notification_data.get('out_trade_no') + txn_id = notification_data.get('trade_no') if not reference or not txn_id: raise ValidationError( "Alipay: " + _( @@ -101,21 +100,21 @@ class PaymentTransaction(models.Model): return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Alipay data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'alipay': return - self.acquirer_reference = data.get('trade_no') - status = data.get('trade_status') + self.acquirer_reference = notification_data.get('trade_no') + status = notification_data.get('trade_status') if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']: self._set_done() elif status == 'TRADE_CLOSED': diff --git a/addons/payment_alipay/tests/test_alipay.py b/addons/payment_alipay/tests/test_alipay.py index fa2f8921984..ef13d3000be 100644 --- a/addons/payment_alipay/tests/test_alipay.py +++ b/addons/payment_alipay/tests/test_alipay.py @@ -141,11 +141,13 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon): def _test_alipay_feedback_processing(self): # Unknown transaction with self.assertRaises(ValidationError): - self.env['payment.transaction']._handle_feedback_data('alipay', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data( + 'alipay', self.NOTIFICATION_DATA + ) # Confirmed transaction tx = self.create_transaction('redirect') - self.env['payment.transaction']._handle_feedback_data('alipay', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data('alipay', self.NOTIFICATION_DATA) self.assertEqual(tx.state, 'done') self.assertEqual(tx.acquirer_reference, self.NOTIFICATION_DATA['trade_no']) @@ -155,7 +157,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon): payload = dict( self.NOTIFICATION_DATA, out_trade_no=self.reference, trade_status='TRADE_CLOSED' ) - self.env['payment.transaction']._handle_feedback_data('alipay', payload) + self.env['payment.transaction']._handle_notification_data('alipay', payload) self.assertEqual(tx.state, 'cancel') @mute_logger('odoo.addons.payment_alipay.controllers.main') @@ -188,7 +190,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon): '._verify_notification_signature' ) as signature_check_mock, patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_http_post_request(url, data=self.NOTIFICATION_DATA) self.assertEqual(origin_check_mock.call_count, 1) diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py index ffca19deb36..0ba84afb035 100644 --- a/addons/payment_authorize/controllers/main.py +++ b/addons/payment_authorize/controllers/main.py @@ -56,10 +56,4 @@ class AuthorizeController(http.Controller): "payment request response for transaction with reference %s:\n%s", reference, pprint.pformat(response_content) ) - # As the API has no redirection flow, we always know the reference of the transaction. - # Still, we prefer to simulate the matching of the transaction by crafting dummy feedback - # data in order to go through the centralized `_handle_feedback_data` method. - feedback_data = {'reference': tx_sudo.reference, 'response': response_content} - request.env['payment.transaction'].sudo()._handle_feedback_data( - 'authorize', feedback_data - ) + tx_sudo._handle_notification_data('authorize', {'response': response_content}) diff --git a/addons/payment_authorize/models/payment_transaction.py b/addons/payment_authorize/models/payment_transaction.py index 7f68f2bd296..f75b0970916 100644 --- a/addons/payment_authorize/models/payment_transaction.py +++ b/addons/payment_authorize/models/payment_transaction.py @@ -78,12 +78,7 @@ class PaymentTransaction(models.Model): "auth_and_capture request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(res_content) ) - - # As the API has no redirection flow, we always know the reference of the transaction. - # Still, we prefer to simulate the matching of the transaction by crafting dummy feedback - # data in order to go through the centralized `_handle_feedback_data` method. - feedback_data = {'reference': self.reference, 'response': res_content} - self._handle_feedback_data('authorize', feedback_data) + self._handle_notification_data('authorize', {'response': res_content}) def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True): """ Override of payment to send a refund request to Authorize. @@ -112,12 +107,7 @@ class PaymentTransaction(models.Model): "refund request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(res_content) ) - # As the API has no redirection flow, we always know the reference of the transaction. - # Still, we prefer to simulate the matching of the transaction by crafting dummy feedback - # data in order to go through the centralized `_handle_feedback_data` method. - feedback_data = {'reference': self.reference, 'response': res_content} - self._handle_feedback_data('authorize', feedback_data) - + self._handle_notification_data('authorize', {'response': res_content}) return refund_tx def _send_capture_request(self): @@ -138,11 +128,7 @@ class PaymentTransaction(models.Model): "capture request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(res_content) ) - # As the API has no redirection flow, we always know the reference of the transaction. - # Still, we prefer to simulate the matching of the transaction by crafting dummy feedback - # data in order to go through the centralized `_handle_feedback_data` method. - feedback_data = {'reference': self.reference, 'response': res_content} - self._handle_feedback_data('authorize', feedback_data) + self._handle_notification_data('authorize', {'response': res_content}) def _send_void_request(self): """ Override of payment to send a void request to Authorize. @@ -161,26 +147,21 @@ class PaymentTransaction(models.Model): "void request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(res_content) ) - # As the API has no redirection flow, we always know the reference of the transaction. - # Still, we prefer to simulate the matching of the transaction by crafting dummy feedback - # data in order to go through the centralized `_handle_feedback_data` method. - feedback_data = {'reference': self.reference, 'response': res_content} - self._handle_feedback_data('authorize', feedback_data) + self._handle_notification_data('authorize', {'response': res_content}) - @api.model - def _get_tx_from_feedback_data(self, provider, data): - """ Find the transaction based on the feedback data. + def _get_tx_from_notification_data(self, provider, notification_data): + """ Find the transaction based on Authorize.net data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the acquirer + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'authorize': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'authorize' or len(tx) == 1: return tx - reference = data.get('reference') + reference = notification_data.get('reference') tx = self.search([('reference', '=', reference), ('provider', '=', 'authorize')]) if not tx: raise ValidationError( @@ -188,19 +169,19 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Authorize data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'authorize': return - response_content = data.get('response') + response_content = notification_data.get('response') self.acquirer_reference = response_content.get('x_trans_id') status_code = response_content.get('x_response_code', '3') @@ -215,7 +196,7 @@ class PaymentTransaction(models.Model): if self.tokenize and not self.token_id: self._authorize_tokenize() if self.operation == 'validation': - # Void the transaction. In last step because it calls _handle_feedback_data() + # Void the transaction. In last step because it calls _handle_notification_data. self._send_refund_request(create_refund_transaction=False) elif status_type == 'void': if self.operation == 'validation': # Validation txs are authorized and then voided diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py index 069416a7e9e..3a91ca2634d 100644 --- a/addons/payment_buckaroo/controllers/main.py +++ b/addons/payment_buckaroo/controllers/main.py @@ -38,13 +38,13 @@ class BuckarooController(http.Controller): # Check the integrity of the notification received_signature = data.get('brq_signature') - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'buckaroo', data ) self._verify_notification_signature(raw_data, received_signature, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('buckaroo', data) + tx_sudo._handle_notification_data('buckaroo', data) return request.redirect('/payment/status') @http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False) @@ -62,13 +62,13 @@ class BuckarooController(http.Controller): try: # Check the integrity of the notification received_signature = data.get('brq_signature') - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'buckaroo', data ) self._verify_notification_signature(raw_data, received_signature, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('buckaroo', data) + tx_sudo._handle_notification_data('buckaroo', data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") return '' diff --git a/addons/payment_buckaroo/models/payment_transaction.py b/addons/payment_buckaroo/models/payment_transaction.py index 2d196d58d0f..7d62d50cf2d 100644 --- a/addons/payment_buckaroo/models/payment_transaction.py +++ b/addons/payment_buckaroo/models/payment_transaction.py @@ -49,21 +49,20 @@ class PaymentTransaction(models.Model): ) return rendering_values - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Buckaroo data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The normalized feedback data sent by the provider + :param dict notification_data: The normalized notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'buckaroo': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'buckaroo' or len(tx) == 1: return tx - reference = data.get('brq_invoicenumber') + reference = notification_data.get('brq_invoicenumber') tx = self.search([('reference', '=', reference), ('provider', '=', 'buckaroo')]) if not tx: raise ValidationError( @@ -72,27 +71,27 @@ class PaymentTransaction(models.Model): return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Buckaroo data. Note: self.ensure_one() - :param dict data: The normalized feedback data sent by the provider + :param dict notification_data: The normalized notification data sent by the provider :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'buckaroo': return - transaction_keys = data.get('brq_transactions') + transaction_keys = notification_data.get('brq_transactions') if not transaction_keys: raise ValidationError("Buckaroo: " + _("Received data with missing transaction keys")) # BRQ_TRANSACTIONS can hold multiple, comma-separated, tx keys. In practice, it holds only # one reference. So we split for semantic correctness and keep the first transaction key. self.acquirer_reference = transaction_keys.split(',')[0] - status_code = int(data.get('brq_statuscode') or 0) + status_code = int(notification_data.get('brq_statuscode') or 0) if status_code in STATUS_CODES_MAPPING['pending']: self._set_pending() elif status_code in STATUS_CODES_MAPPING['done']: diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py index 66ee43eafd2..6d506a2af15 100644 --- a/addons/payment_buckaroo/tests/test_buckaroo.py +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -43,10 +43,10 @@ class BuckarooTest(BuckarooCommon, PaymentHttpCommon): def test_feedback_processing(self): notification_data = BuckarooController._normalize_data_keys(self.SYNC_NOTIFICATION_DATA) tx = self.create_transaction(flow='redirect') - tx._handle_feedback_data('buckaroo', notification_data) + tx._handle_notification_data('buckaroo', notification_data) self.assertEqual(tx.state, 'done') self.assertEqual(tx.acquirer_reference, notification_data.get('brq_transactions')) - tx._handle_feedback_data('buckaroo', notification_data) + tx._handle_notification_data('buckaroo', notification_data) self.assertEqual(tx.state, 'done', 'Buckaroo: validation did not put tx into done state') self.assertEqual(tx.acquirer_reference, notification_data.get('brq_transactions')) @@ -58,7 +58,7 @@ class BuckarooTest(BuckarooCommon, PaymentHttpCommon): brq_statuscode='2', brq_signature='b8e54e26b2b5a5e697b8ed5085329ea712fd48b2', )) - self.env['payment.transaction']._handle_feedback_data('buckaroo', notification_data) + self.env['payment.transaction']._handle_notification_data('buckaroo', notification_data) self.assertEqual(tx.state, 'error') @mute_logger('odoo.addons.payment_buckaroo.controllers.main') diff --git a/addons/payment_mollie/controllers/main.py b/addons/payment_mollie/controllers/main.py index a05a31d7fb5..f41fd9915b0 100755 --- a/addons/payment_mollie/controllers/main.py +++ b/addons/payment_mollie/controllers/main.py @@ -29,25 +29,25 @@ class MollieController(http.Controller): will satisfy any specification of the `SameSite` attribute, the session of the user will be retrieved and with it the transaction which will be immediately post-processed. - :param dict data: The feedback data (only `id`) and the transaction reference (`ref`) + :param dict data: The notification data (only `id`) and the transaction reference (`ref`) embedded in the return URL """ _logger.info("handling redirection from Mollie with data:\n%s", pprint.pformat(data)) - request.env['payment.transaction'].sudo()._handle_feedback_data('mollie', data) + request.env['payment.transaction'].sudo()._handle_notification_data('mollie', data) return request.redirect('/payment/status') @http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False) def mollie_webhook(self, **data): """ Process the notification data sent by Mollie to the webhook. - :param dict data: The feedback data (only `id`) and the transaction reference (`ref`) + :param dict data: The notification data (only `id`) and the transaction reference (`ref`) embedded in the return URL :return: An empty string to acknowledge the notification :rtype: str """ _logger.info("notification received from Mollie with data:\n%s", pprint.pformat(data)) try: - request.env['payment.transaction'].sudo()._handle_feedback_data('mollie', data) + request.env['payment.transaction'].sudo()._handle_notification_data('mollie', data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") return '' # Acknowledge the notification diff --git a/addons/payment_mollie/models/payment_transaction.py b/addons/payment_mollie/models/payment_transaction.py index 055ba9a726f..3ce46f89c7d 100644 --- a/addons/payment_mollie/models/payment_transaction.py +++ b/addons/payment_mollie/models/payment_transaction.py @@ -65,35 +65,37 @@ class PaymentTransaction(models.Model): 'webhookUrl': f'{webhook_url}?ref={self.reference}', } - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Mollie data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'mollie': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'mollie' or len(tx) == 1: return tx - tx = self.search([('reference', '=', data.get('ref')), ('provider', '=', 'mollie')]) + tx = self.search( + [('reference', '=', notification_data.get('ref')), ('provider', '=', 'mollie')] + ) if not tx: - raise ValidationError( - "Mollie: " + _("No transaction found matching reference %s.", data.get('ref')) - ) + raise ValidationError("Mollie: " + _( + "No transaction found matching reference %s.", notification_data.get('ref') + )) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Mollie data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'mollie': return diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py index 6bec40eae6b..805e7385cdc 100644 --- a/addons/payment_ogone/controllers/main.py +++ b/addons/payment_ogone/controllers/main.py @@ -42,13 +42,13 @@ class OgoneController(http.Controller): # Check the integrity of the notification received_signature = data.get('SHASIGN') - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'ogone', data ) self._verify_notification_signature(raw_data, received_signature, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data) + tx_sudo._handle_notification_data('ogone', data) return request.redirect('/payment/status') @staticmethod diff --git a/addons/payment_ogone/models/payment_transaction.py b/addons/payment_ogone/models/payment_transaction.py index fa59f173b60..9d195ec15fe 100644 --- a/addons/payment_ogone/models/payment_transaction.py +++ b/addons/payment_ogone/models/payment_transaction.py @@ -110,7 +110,6 @@ class PaymentTransaction(models.Model): raise UserError("Ogone: " + _("The transaction is not linked to a token.")) # Make the payment request - base_url = self.acquirer_id.get_base_url() data = { # DirectLink parameters 'PSPID': self.acquirer_id.ogone_pspid, @@ -154,23 +153,22 @@ class PaymentTransaction(models.Model): "handling feedback data from Ogone for transaction with reference %s with data:\n%s", self.reference, pprint.pformat(feedback_data) ) - self._handle_feedback_data('ogone', feedback_data) + self._handle_notification_data('ogone', feedback_data) - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Ogone data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'ogone': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'ogone' or len(tx) == 1: return tx - reference = data.get('ORDERID') + reference = notification_data.get('ORDERID') tx = self.search([('reference', '=', reference), ('provider', '=', 'ogone')]) if not tx: raise ValidationError( @@ -178,37 +176,37 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Ogone data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'ogone': return - if 'tree' in data: - data = data['tree'] + if 'tree' in notification_data: + notification_data = notification_data['tree'] - self.acquirer_reference = data.get('PAYID') - payment_status = int(data.get('STATUS', '0')) + self.acquirer_reference = notification_data.get('PAYID') + payment_status = int(notification_data.get('STATUS', '0')) if payment_status in const.PAYMENT_STATUS_MAPPING['pending']: self._set_pending() elif payment_status in const.PAYMENT_STATUS_MAPPING['done']: - has_token_data = 'ALIAS' in data + has_token_data = 'ALIAS' in notification_data if self.tokenize and has_token_data: - self._ogone_tokenize_from_feedback_data(data) + self._ogone_tokenize_from_notification_data(notification_data) self._set_done() elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']: self._set_canceled() elif payment_status in const.PAYMENT_STATUS_MAPPING['declined']: - if data.get("NCERRORPLUS"): - reason = data.get("NCERRORPLUS") - elif data.get("NCERROR"): - reason = "Error code: %s" % data.get("NCERROR") + if notification_data.get("NCERRORPLUS"): + reason = notification_data.get("NCERRORPLUS") + elif notification_data.get("NCERROR"): + reason = "Error code: %s" % notification_data.get("NCERROR") else: reason = "Unknown reason" _logger.info("the payment has been declined: %s.", reason) @@ -224,18 +222,18 @@ class PaymentTransaction(models.Model): "Ogone: " + _("Received data with invalid payment status: %s", payment_status) ) - def _ogone_tokenize_from_feedback_data(self, data): - """ Create a token from feedback data. + def _ogone_tokenize_from_notification_data(self, notification_data): + """ Create a token from notification data. - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - token_name = data.get('CARDNO') or payment_utils.build_token_name() + token_name = notification_data.get('CARDNO') or payment_utils.build_token_name() token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, 'name': token_name, # Already padded with 'X's 'partner_id': self.partner_id.id, - 'acquirer_ref': data['ALIAS'], + 'acquirer_ref': notification_data['ALIAS'], 'verified': True, # The payment is authorized, so the payment method is valid }) self.write({ diff --git a/addons/payment_ogone/tests/test_ogone.py b/addons/payment_ogone/tests/test_ogone.py index 8e7aee2d2e9..753a265ba70 100644 --- a/addons/payment_ogone/tests/test_ogone.py +++ b/addons/payment_ogone/tests/test_ogone.py @@ -122,7 +122,7 @@ class OgoneTest(OgoneCommon, PaymentHttpCommon): '._verify_notification_signature' ) as signature_check_mock, patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_http_post_request(url, data=self.NOTIFICATION_DATA) self.assertEqual(signature_check_mock.call_count, 1) diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index f4184eefc17..3e952436df6 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -50,7 +50,7 @@ class PaypalController(http.Controller): pass # Redirect them to the status page to browse the (currently) draft transaction else: # Check the origin of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'paypal', pdt_data ) try: @@ -59,9 +59,7 @@ class PaypalController(http.Controller): _logger.exception("could not verify the origin of the PDT; discarding it") else: # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data( - 'paypal', notification_data - ) + tx_sudo._handle_notification_data('paypal', notification_data) return request.redirect('/payment/status') @@ -144,13 +142,13 @@ class PaypalController(http.Controller): _logger.info("notification received from PayPal with data:\n%s", pprint.pformat(data)) try: # Check the origin and integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'paypal', data ) self._verify_webhook_notification_origin(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('paypal', data) + tx_sudo._handle_notification_data('paypal', data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") return '' diff --git a/addons/payment_paypal/models/payment_transaction.py b/addons/payment_paypal/models/payment_transaction.py index 5c852a96912..18c7f93371b 100644 --- a/addons/payment_paypal/models/payment_transaction.py +++ b/addons/payment_paypal/models/payment_transaction.py @@ -58,21 +58,20 @@ class PaymentTransaction(models.Model): 'api_url': self.acquirer_id._paypal_get_api_url(), } - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Paypal data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'paypal': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'paypal' or len(tx) == 1: return tx - reference = data.get('item_number') + reference = notification_data.get('item_number') tx = self.search([('reference', '=', reference), ('provider', '=', 'paypal')]) if not tx: raise ValidationError( @@ -80,21 +79,21 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Paypal data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'paypal': return - txn_id = data.get('txn_id') - txn_type = data.get('txn_type') + txn_id = notification_data.get('txn_id') + txn_type = notification_data.get('txn_type') if not all((txn_id, txn_type)): raise ValidationError( "PayPal: " + _( @@ -105,7 +104,7 @@ class PaymentTransaction(models.Model): self.acquirer_reference = txn_id self.paypal_type = txn_type - payment_status = data.get('payment_status') + payment_status = notification_data.get('payment_status') if payment_status in PAYMENT_STATUS_MAPPING['pending'] + PAYMENT_STATUS_MAPPING['done'] \ and not (self.acquirer_id.paypal_pdt_token and self.acquirer_id.paypal_seller_account): @@ -113,7 +112,7 @@ class PaymentTransaction(models.Model): self.acquirer_id._paypal_send_configuration_reminder() if payment_status in PAYMENT_STATUS_MAPPING['pending']: - self._set_pending(state_message=data.get('pending_reason')) + self._set_pending(state_message=notification_data.get('pending_reason')) elif payment_status in PAYMENT_STATUS_MAPPING['done']: self._set_done() elif payment_status in PAYMENT_STATUS_MAPPING['cancel']: diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py index 8240bdb5706..ba40676f547 100644 --- a/addons/payment_paypal/tests/test_paypal.py +++ b/addons/payment_paypal/tests/test_paypal.py @@ -83,11 +83,11 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon): def test_feedback_processing(self): # Unknown transaction with self.assertRaises(ValidationError): - self.env['payment.transaction']._handle_feedback_data('paypal', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data('paypal', self.NOTIFICATION_DATA) # Confirmed transaction tx = self.create_transaction('redirect') - self.env['payment.transaction']._handle_feedback_data('paypal', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data('paypal', self.NOTIFICATION_DATA) self.assertEqual(tx.state, 'done') self.assertEqual(tx.acquirer_reference, self.NOTIFICATION_DATA['txn_id']) @@ -100,7 +100,7 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon): payment_status='Pending', pending_reason='multi_currency', ) - self.env['payment.transaction']._handle_feedback_data('paypal', payload) + self.env['payment.transaction']._handle_notification_data('paypal', payload) self.assertEqual(tx.state, 'pending') self.assertEqual(tx.state_message, payload['pending_reason']) @@ -150,7 +150,7 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon): '._verify_webhook_notification_origin' ) as origin_check_mock, patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_http_post_request(url, data=self.NOTIFICATION_DATA) self.assertEqual(origin_check_mock.call_count, 1) diff --git a/addons/payment_payulatam/controllers/main.py b/addons/payment_payulatam/controllers/main.py index 86e1d2d2be7..fce44ee736a 100644 --- a/addons/payment_payulatam/controllers/main.py +++ b/addons/payment_payulatam/controllers/main.py @@ -15,5 +15,5 @@ class PayuLatamController(http.Controller): @http.route(_return_url, type='http', auth='public', methods=['GET']) def payulatam_return(self, **data): _logger.info("handling redirection from PayU Latam with data:\n%s", pprint.pformat(data)) - request.env['payment.transaction'].sudo()._handle_feedback_data('payulatam', data) + request.env['payment.transaction'].sudo()._handle_notification_data('payulatam', data) return request.redirect('/payment/status') diff --git a/addons/payment_payulatam/models/payment_transaction.py b/addons/payment_payulatam/models/payment_transaction.py index 32973b91e26..b0193b2ca07 100644 --- a/addons/payment_payulatam/models/payment_transaction.py +++ b/addons/payment_payulatam/models/payment_transaction.py @@ -84,24 +84,23 @@ class PaymentTransaction(models.Model): ) return payulatam_values - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Payulatam data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received :raise: ValidationError if the data match no transaction :raise: ValidationError if the signature can not be verified """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'payulatam': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'payulatam' or len(tx) == 1: return tx - reference = data.get('referenceCode') - sign = data.get('signature') + reference = notification_data.get('referenceCode') + sign = notification_data.get('signature') if not reference or not sign: raise ValidationError( "PayU Latam: " + _( @@ -117,7 +116,7 @@ class PaymentTransaction(models.Model): ) # Verify signature - sign_check = tx.acquirer_id._payulatam_generate_sign(data, incoming=True) + sign_check = tx.acquirer_id._payulatam_generate_sign(notification_data, incoming=True) if not hmac.compare_digest(sign_check, sign): raise ValidationError( "PayU Latam: " + _( @@ -128,22 +127,22 @@ class PaymentTransaction(models.Model): return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Payulatam data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'payulatam': return - self.acquirer_reference = data.get('transactionId') + self.acquirer_reference = notification_data.get('transactionId') - status = data.get('lapTransactionState') - state_message = data.get('message') + status = notification_data.get('lapTransactionState') + state_message = notification_data.get('message') if status == 'PENDING': self._set_pending(state_message=state_message) elif status == 'APPROVED': diff --git a/addons/payment_payulatam/tests/test_payulatam.py b/addons/payment_payulatam/tests/test_payulatam.py index 2f6780abbc5..b8449f9062c 100644 --- a/addons/payment_payulatam/tests/test_payulatam.py +++ b/addons/payment_payulatam/tests/test_payulatam.py @@ -124,12 +124,14 @@ class PayULatamTest(PayULatamCommon): # should raise error about unknown tx with self.assertRaises(ValidationError): - self.env['payment.transaction']._handle_feedback_data('payulatam', payulatam_post_data) + self.env['payment.transaction']._handle_notification_data( + 'payulatam', payulatam_post_data + ) tx = self.create_transaction(flow='redirect') # Validate the transaction ('pending' state) - self.env['payment.transaction']._handle_feedback_data('payulatam', payulatam_post_data) + self.env['payment.transaction']._handle_notification_data('payulatam', payulatam_post_data) self.assertEqual(tx.state, 'pending', 'Payulatam: wrong state after receiving a valid pending notification') self.assertEqual(tx.state_message, payulatam_post_data['message'], 'Payulatam: wrong state message after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, 'b232989a-4aa8-42d1-bace-153236eee791', 'Payulatam: wrong txn_id after receiving a valid pending notification') @@ -141,7 +143,7 @@ class PayULatamTest(PayULatamCommon): # Validate the transaction ('approved' state) payulatam_post_data['lapTransactionState'] = 'APPROVED' - self.env['payment.transaction']._handle_feedback_data('payulatam', payulatam_post_data) + self.env['payment.transaction']._handle_notification_data('payulatam', payulatam_post_data) self.assertEqual(tx.state, 'done', 'Payulatam: wrong state after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, 'b232989a-4aa8-42d1-bace-153236eee791', 'Payulatam: wrong txn_id after receiving a valid pending notification') diff --git a/addons/payment_payumoney/controllers/main.py b/addons/payment_payumoney/controllers/main.py index 31116ac6dc1..4571d3dd556 100644 --- a/addons/payment_payumoney/controllers/main.py +++ b/addons/payment_payumoney/controllers/main.py @@ -37,13 +37,13 @@ class PayUMoneyController(http.Controller): _logger.info("handling redirection from PayU money with data:\n%s", pprint.pformat(data)) # Check the integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'payumoney', data ) self._verify_notification_signature(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('payumoney', data) + tx_sudo._handle_notification_data('payumoney', data) return request.redirect('/payment/status') @staticmethod diff --git a/addons/payment_payumoney/models/payment_transaction.py b/addons/payment_payumoney/models/payment_transaction.py index 7c46b9e88f4..240ceb7383c 100644 --- a/addons/payment_payumoney/models/payment_transaction.py +++ b/addons/payment_payumoney/models/payment_transaction.py @@ -45,22 +45,21 @@ class PaymentTransaction(models.Model): ) return payumoney_values - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Payumoney data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'payumoney': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'payumoney' or len(tx) == 1: return tx - reference = data.get('txnid') + reference = notification_data.get('txnid') if not reference: raise ValidationError( "PayUmoney: " + _("Received data with missing reference (%s)", reference) @@ -74,26 +73,26 @@ class PaymentTransaction(models.Model): return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Payumoney data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'payumoney': return - status = data.get('status') - self.acquirer_reference = data.get('payuMoneyId') + status = notification_data.get('status') + self.acquirer_reference = notification_data.get('payuMoneyId') if status == 'success': self._set_done() else: # 'failure' # See https://www.payumoney.com/pdf/PayUMoney-Technical-Integration-Document.pdf - error_code = data.get('Error') + error_code = notification_data.get('Error') self._set_error( "PayUmoney: " + _("The payment encountered an error with code %s", error_code) ) diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py index 7b01ffacdce..7bbe97eec49 100644 --- a/addons/payment_sips/controllers/main.py +++ b/addons/payment_sips/controllers/main.py @@ -37,13 +37,13 @@ class SipsController(http.Controller): _logger.info("handling redirection from SIPS with data:\n%s", pprint.pformat(data)) # Check the integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'sips', data ) self._verify_notification_signature(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('sips', data) + tx_sudo._handle_notification_data('sips', data) return request.redirect('/payment/status') @http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False) @@ -57,13 +57,13 @@ class SipsController(http.Controller): _logger.info("notification received from SIPS with data:\n%s", pprint.pformat(data)) try: # Check the integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'sips', data ) self._verify_notification_signature(data, tx_sudo) # Handle the notification data - request.env['payment.transaction'].sudo()._handle_feedback_data('sips', data) + tx_sudo._handle_notification_data('sips', data) except ValidationError: _logger.exception("unable to handle the notification data; skipping to acknowledge") return '' diff --git a/addons/payment_sips/models/payment_transaction.py b/addons/payment_sips/models/payment_transaction.py index 9b7171f0311..390c808d088 100644 --- a/addons/payment_sips/models/payment_transaction.py +++ b/addons/payment_sips/models/payment_transaction.py @@ -82,21 +82,20 @@ class PaymentTransaction(models.Model): 'Seal': self.acquirer_id._sips_generate_shasign(data), } - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Sips data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'sips': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'sips' or len(tx) == 1: return tx - data = self._sips_data_to_object(data['Data']) + data = self._sips_notification_data_to_object(notification_data['Data']) reference = data.get('transactionReference') if not reference: @@ -111,19 +110,19 @@ class PaymentTransaction(models.Model): return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Sips data. Note: self.ensure_one() - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'sips': return - data = self._sips_data_to_object(data.get('Data')) + data = self._sips_notification_data_to_object(notification_data.get('Data')) self.acquirer_reference = data.get('transactionReference') response_code = data.get('responseCode') if response_code in RESPONSE_CODES_MAPPING['pending']: @@ -148,7 +147,7 @@ class PaymentTransaction(models.Model): }, ) - def _sips_data_to_object(self, data): + def _sips_notification_data_to_object(self, data): res = {} for element in data.split('|'): key, value = element.split('=') diff --git a/addons/payment_sips/tests/test_sips.py b/addons/payment_sips/tests/test_sips.py index cd52838cfe2..eb7b22f99d3 100644 --- a/addons/payment_sips/tests/test_sips.py +++ b/addons/payment_sips/tests/test_sips.py @@ -70,11 +70,13 @@ class SipsTest(SipsCommon, PaymentHttpCommon): def test_feedback_processing(self): # Unknown transaction with self.assertRaises(ValidationError): - self.env['payment.transaction']._handle_feedback_data('sips', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data( + 'sips', self.NOTIFICATION_DATA + ) # Confirmed transaction tx = self.create_transaction('redirect') - self.env['payment.transaction']._handle_feedback_data('sips', self.NOTIFICATION_DATA) + self.env['payment.transaction']._handle_notification_data('sips', self.NOTIFICATION_DATA) self.assertEqual(tx.state, 'done') self.assertEqual(tx.acquirer_reference, self.reference) @@ -87,7 +89,7 @@ class SipsTest(SipsCommon, PaymentHttpCommon): Data=self.NOTIFICATION_DATA['Data'].replace(old_reference, self.reference) .replace('responseCode=00', 'responseCode=12') ) - self.env['payment.transaction']._handle_feedback_data('sips', payload) + self.env['payment.transaction']._handle_notification_data('sips', payload) self.assertEqual(tx.state, 'cancel') @mute_logger('odoo.addons.payment_sips.controllers.main') @@ -112,7 +114,7 @@ class SipsTest(SipsCommon, PaymentHttpCommon): '._verify_notification_signature' ) as signature_check_mock, patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_http_post_request(url, data=self.NOTIFICATION_DATA) self.assertEqual(signature_check_mock.call_count, 1) diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py index 9e6695cc68a..ac838c48905 100644 --- a/addons/payment_stripe/controllers/main.py +++ b/addons/payment_stripe/controllers/main.py @@ -29,7 +29,7 @@ class StripeController(http.Controller): :param dict data: The GET params appended to the URL in `_stripe_create_checkout_session` """ # Retrieve the tx based on the tx reference included in the return url - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'stripe', data ) @@ -38,10 +38,10 @@ class StripeController(http.Controller): f'payment_intents/{tx_sudo.stripe_payment_intent}', method='GET' ) _logger.info("received payment_intents response:\n%s", pprint.pformat(payment_intent)) - self._include_payment_intent_in_feedback_data(payment_intent, data) + self._include_payment_intent_in_notification_data(payment_intent, data) - # Handle the feedback data crafted with Stripe API objects - request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data) + # Handle the notification data crafted with Stripe API objects + tx_sudo._handle_notification_data('stripe', data) # Redirect the user to the status page return request.redirect('/payment/status') @@ -53,7 +53,7 @@ class StripeController(http.Controller): :param dict data: The GET params appended to the URL in `_stripe_create_checkout_session` """ # Retrieve the transaction based on the tx reference included in the return url - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'stripe', data ) @@ -64,10 +64,12 @@ class StripeController(http.Controller): method='GET' ) _logger.info("received checkout/session response:\n%s", pprint.pformat(checkout_session)) - self._include_setup_intent_in_feedback_data(checkout_session.get('setup_intent', {}), data) + self._include_setup_intent_in_notification_data( + checkout_session.get('setup_intent', {}), data + ) - # Handle the feedback data crafted with Stripe API objects - request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data) + # Handle the notification data crafted with Stripe API objects + tx_sudo._handle_notification_data('stripe', data) # Redirect the user to the status page return request.redirect('/payment/status') @@ -87,7 +89,7 @@ class StripeController(http.Controller): # Check the integrity of the event data = {'reference': checkout_session['client_reference_id']} - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( 'stripe', data ) self._verify_notification_signature(tx_sudo) @@ -99,7 +101,7 @@ class StripeController(http.Controller): _logger.info( "received payment_intents response:\n%s", pprint.pformat(payment_intent) ) - self._include_payment_intent_in_feedback_data(payment_intent, data) + self._include_payment_intent_in_notification_data(payment_intent, data) # Fetch the SetupIntent and PaymentMethod objects from Stripe if checkout_session.get('setup_intent'): # Can be None @@ -111,27 +113,27 @@ class StripeController(http.Controller): _logger.info( "received setup_intents response:\n%s", pprint.pformat(setup_intent) ) - self._include_setup_intent_in_feedback_data(setup_intent, data) + self._include_setup_intent_in_notification_data(setup_intent, data) - # Handle the feedback data crafted with Stripe API objects as a regular feedback - request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data) + # Handle the notification data crafted with Stripe API objects + tx_sudo._handle_notification_data('stripe', data) except ValidationError: # Acknowledge the notification to avoid getting spammed _logger.exception("unable to handle the notification data; skipping to acknowledge") return '' @staticmethod - def _include_payment_intent_in_feedback_data(payment_intent, data): - data.update({'payment_intent': payment_intent}) + def _include_payment_intent_in_notification_data(payment_intent, notification_data): + notification_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 - data.update({ + notification_data.update({ 'charge': charge, 'payment_method': charge.get('payment_method_details'), }) @staticmethod - def _include_setup_intent_in_feedback_data(setup_intent, data): - data.update({ + def _include_setup_intent_in_notification_data(setup_intent, notification_data): + notification_data.update({ 'setup_intent': setup_intent, 'payment_method': setup_intent.get('payment_method') }) diff --git a/addons/payment_stripe/models/payment_transaction.py b/addons/payment_stripe/models/payment_transaction.py index d4a148e2b7d..b6baea51d39 100644 --- a/addons/payment_stripe/models/payment_transaction.py +++ b/addons/payment_stripe/models/payment_transaction.py @@ -167,12 +167,12 @@ class PaymentTransaction(models.Model): payment_intent = self._stripe_create_payment_intent() feedback_data = {'reference': self.reference} - StripeController._include_payment_intent_in_feedback_data(payment_intent, feedback_data) + StripeController._include_payment_intent_in_notification_data(payment_intent, feedback_data) _logger.info( "payment request response for transaction with reference %s:\n%s", self.reference, pprint.pformat(feedback_data) ) - self._handle_feedback_data('stripe', feedback_data) + self._handle_notification_data('stripe', feedback_data) def _stripe_create_payment_intent(self): """ Create and return a PaymentIntent. @@ -210,22 +210,21 @@ class PaymentTransaction(models.Model): return payment_intent - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on Stripe data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The feedback data sent by the provider + :param dict notification_data: The notification data sent by the provider :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if inconsistent data were received :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'stripe': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'stripe' or len(tx) == 1: return tx - reference = data.get('reference') + reference = notification_data.get('reference') if not reference: raise ValidationError("Stripe: " + _("Received data with missing merchant reference")) @@ -236,30 +235,31 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on Adyen data. Note: self.ensure_one() - :param dict data: The feedback data build from information passed to the return route. - Depending on the operation of the transaction, the entries with the keys - 'payment_intent', 'charge', 'setup_intent' and 'payment_method' can be - populated with their corresponding Stripe API objects. + :param dict notification_data: The notification data build from information passed to the + return route. Depending on the operation of the transaction, + the entries with the keys 'payment_intent', 'charge', + 'setup_intent' and 'payment_method' can be populated with + their corresponding Stripe API objects. :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'stripe': return - if 'charge' in data: - self.acquirer_reference = data['charge']['id'] + if 'charge' in notification_data: + self.acquirer_reference = notification_data['charge']['id'] # Handle the intent status if self.operation == 'validation': - intent_status = data.get('setup_intent', {}).get('status') + intent_status = notification_data.get('setup_intent', {}).get('status') else: # 'online_redirect', 'online_token', 'offline' - intent_status = data.get('payment_intent', {}).get('status') + intent_status = notification_data.get('payment_intent', {}).get('status') if not intent_status: raise ValidationError( "Stripe: " + _("Received data with missing intent status.") @@ -271,7 +271,7 @@ class PaymentTransaction(models.Model): self._set_pending() elif intent_status in INTENT_STATUS_MAPPING['done']: if self.tokenize: - self._stripe_tokenize_from_feedback_data(data) + self._stripe_tokenize_from_notification_data(notification_data) self._set_done() elif intent_status in INTENT_STATUS_MAPPING['cancel']: self._set_canceled() @@ -284,21 +284,25 @@ class PaymentTransaction(models.Model): "Stripe: " + _("Received data with invalid intent status: %s", intent_status) ) - def _stripe_tokenize_from_feedback_data(self, data): - """ Create a new token based on the feedback data. + def _stripe_tokenize_from_notification_data(self, notification_data): + """ Create a new token based on the notification data. - :param dict data: The feedback data built with Stripe objects. See `_process_feedback_data`. + :param dict notification_data: The notification data built with Stripe objects. + See `_process_notification_data`. :return: None """ if self.operation == 'online_redirect': - payment_method_id = data.get('charge', {}).get('payment_method') - customer_id = data.get('charge', {}).get('customer') + payment_method_id = notification_data.get('charge', {}).get('payment_method') + customer_id = notification_data.get('charge', {}).get('customer') else: # 'validation' - payment_method_id = data.get('setup_intent', {}).get('payment_method', {}).get('id') - customer_id = data.get('setup_intent', {}).get('customer') - payment_method = data.get('payment_method') + payment_method_id = notification_data.get('setup_intent', {}) \ + .get('payment_method', {}).get('id') + customer_id = notification_data.get('setup_intent', {}).get('customer') + payment_method = notification_data.get('payment_method') if not payment_method_id or not payment_method: - _logger.warning("requested tokenization with payment method missing from feedback data") + _logger.warning( + "requested tokenization from notification data with missing payment method" + ) return if payment_method.get('type') != 'card': diff --git a/addons/payment_stripe/tests/test_stripe.py b/addons/payment_stripe/tests/test_stripe.py index 08eaedabcbc..82db3711050 100644 --- a/addons/payment_stripe/tests/test_stripe.py +++ b/addons/payment_stripe/tests/test_stripe.py @@ -60,7 +60,7 @@ class StripeTest(StripeCommon, PaymentHttpCommon): return_value={}, ), patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_feedback_data' + '._handle_notification_data' ): self._make_json_request(url, data=self.NOTIFICATION_DATA) self.assertEqual(signature_check_mock.call_count, 1) diff --git a/addons/payment_test/controllers/main.py b/addons/payment_test/controllers/main.py index 41b7594803c..a187e97a33d 100644 --- a/addons/payment_test/controllers/main.py +++ b/addons/payment_test/controllers/main.py @@ -18,4 +18,6 @@ class PaymentTestController(http.Controller): 'reference': reference, 'cc_summary': customer_input[-4:], } - request.env['payment.transaction'].sudo()._handle_feedback_data('test', fake_api_response) + request.env['payment.transaction'].sudo()._handle_notification_data( + 'test', fake_api_response + ) diff --git a/addons/payment_test/models/payment_transaction.py b/addons/payment_test/models/payment_transaction.py index 4c29c2276fe..c591a1c096b 100644 --- a/addons/payment_test/models/payment_transaction.py +++ b/addons/payment_test/models/payment_transaction.py @@ -24,26 +24,22 @@ class PaymentTransaction(models.Model): if self.provider != 'test': return - # The payment request response would normally transit through the controller but in the end, - # all that interests us is the reference. To avoid making a localhost request, we bypass the - # controller and handle the fake feedback data directly. - self._handle_feedback_data('test', {'reference': self.reference}) + self._handle_notification_data('test', None) - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on dummy data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The dummy feedback data + :param dict notification_data: The dummy notification data :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'test': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'test' or len(tx) == 1: return tx - reference = data.get('reference') + reference = notification_data.get('reference') tx = self.search([('reference', '=', reference), ('provider', '=', 'test')]) if not tx: raise ValidationError( @@ -51,24 +47,25 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on dummy data. Note: self.ensure_one() - :param dict data: The dummy feedback data + :param dict notification_data: The dummy notification data :return: None :raise: ValidationError if inconsistent data were received """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != "test": return self._set_done() # Dummy transactions are always successful if self.tokenize: + payment_details_short = notification_data['cc_summary'] token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': payment_utils.build_token_name(payment_details_short=data['cc_summary']), + 'name': payment_utils.build_token_name(payment_details_short=payment_details_short), 'partner_id': self.partner_id.id, 'acquirer_ref': 'fake acquirer reference', 'verified': True, diff --git a/addons/payment_transfer/controllers/main.py b/addons/payment_transfer/controllers/main.py index da89499d440..9d5914a178c 100644 --- a/addons/payment_transfer/controllers/main.py +++ b/addons/payment_transfer/controllers/main.py @@ -15,5 +15,5 @@ class TransferController(http.Controller): @http.route(_accept_url, type='http', auth='public', methods=['POST'], csrf=False) def transfer_form_feedback(self, **post): _logger.info("handling redirection from Transfer with data:\n%s", pprint.pformat(post)) - request.env['payment.transaction'].sudo()._handle_feedback_data('transfer', post) + request.env['payment.transaction'].sudo()._handle_notification_data('transfer', post) return request.redirect('/payment/status') diff --git a/addons/payment_transfer/models/payment_transaction.py b/addons/payment_transfer/models/payment_transaction.py index 2b90272cf4d..0c9ac876f98 100644 --- a/addons/payment_transfer/models/payment_transaction.py +++ b/addons/payment_transfer/models/payment_transaction.py @@ -31,21 +31,20 @@ class PaymentTransaction(models.Model): 'reference': self.reference, } - @api.model - def _get_tx_from_feedback_data(self, provider, data): + def _get_tx_from_notification_data(self, provider, notification_data): """ Override of payment to find the transaction based on transfer data. :param str provider: The provider of the acquirer that handled the transaction - :param dict data: The transfer feedback data + :param dict notification_data: The notification feedback data :return: The transaction if found :rtype: recordset of `payment.transaction` :raise: ValidationError if the data match no transaction """ - tx = super()._get_tx_from_feedback_data(provider, data) - if provider != 'transfer': + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'transfer' or len(tx) == 1: return tx - reference = data.get('reference') + reference = notification_data.get('reference') tx = self.search([('reference', '=', reference), ('provider', '=', 'transfer')]) if not tx: raise ValidationError( @@ -53,15 +52,15 @@ class PaymentTransaction(models.Model): ) return tx - def _process_feedback_data(self, data): + def _process_notification_data(self, notification_data): """ Override of payment to process the transaction based on transfer data. Note: self.ensure_one() - :param dict data: The transfer feedback data + :param dict notification_data: The transfer data :return: None """ - super()._process_feedback_data(data) + super()._process_notification_data(notification_data) if self.provider != 'transfer': return