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