[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:
@@ -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()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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('=')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user