[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) <anv@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv)
2022-02-02 19:50:49 +00:00
parent 0df4b0029b
commit f4ca7290ac
35 changed files with 319 additions and 345 deletions
+18 -17
View File
@@ -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()
+16 -16
View File
@@ -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")
@@ -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({
+7 -14
View File
@@ -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()
+4 -4
View File
@@ -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")
@@ -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':
+6 -4
View File
@@ -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)
+1 -7
View File
@@ -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})
@@ -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
+4 -4
View File
@@ -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 ''
@@ -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']:
@@ -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')
+4 -4
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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
@@ -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({
+1 -1
View File
@@ -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)
+4 -6
View File
@@ -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 ''
@@ -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']:
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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')
@@ -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':
@@ -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')
+2 -2
View File
@@ -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
@@ -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)
)
+4 -4
View File
@@ -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 ''
@@ -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('=')
+6 -4
View File
@@ -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)
+20 -18
View File
@@ -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')
})
@@ -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':
+1 -1
View File
@@ -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)
+3 -1
View File
@@ -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
)
@@ -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,
+1 -1
View File
@@ -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')
@@ -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