Notification handling in some acquirers presents a subset of the following issues: 1. The signature of synchronous notifications (redirect payloads) is not checked. (Alipay, Authorize, Buckaroo, Mollie, PayU money, PayULatam) 2. When the signature check fails, we raise a ValidationError which counts as an HTTP 200 for some providers (it's not the case if they expect a specific string). (Adyen, Paypal, Sips, Stripe) 3. If a ValidationError is raised when processing the feedback data, it is allowed to bubble up to the provider. (Alipay, Ogone) The issues are respectively addressed as follows: 1. If the acquirer implements payments with redirection, make sure that if either makes a request to the provider to validate the data or that it verifies the signature. Verifying the origin of the request is not enough: the payload must be checked too. 2. Instead of raising ValidationError's, raise an HTTP 403 FORBIDDEN error if the signature check fails. 3. Wrap the call to `_handle_feedback_data` of the webhook method inside a try/except clause to catch any ValidationError, log a warning, and acknowledge the notification to avoid having the provider disable the webhook because of too many failures. task-2688139 task-2693293 closes odoo/odoo#81607 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com> Co-authored-by: Lucie Van Nieuwenhuyze <luvn@odoo.com>
157 lines
6.6 KiB
Python
157 lines
6.6 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
|
|
|
|
import json
|
|
import logging
|
|
|
|
from werkzeug import urls
|
|
|
|
from odoo import _, api, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
from odoo.addons.payment import utils as payment_utils
|
|
from odoo.addons.payment_sips.controllers.main import SipsController
|
|
from .const import RESPONSE_CODES_MAPPING, SUPPORTED_CURRENCIES
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentTransaction(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
@api.model
|
|
def _compute_reference(self, provider, prefix=None, separator='-', **kwargs):
|
|
""" Override of payment to ensure that Sips requirements for references are satisfied.
|
|
|
|
Sips requirements for transaction are as follows:
|
|
- References can only be made of alphanumeric characters.
|
|
This is satisfied by forcing the custom separator to 'x' to ensure that no '-' character
|
|
will be used to append a suffix. Additionally, the prefix is sanitized if it was provided,
|
|
and generated with 'tx' as default otherwise. This prevents the prefix to be generated
|
|
based on document names that may contain non-alphanum characters (eg: INV/2020/...).
|
|
- References must be unique at provider level for a given merchant account.
|
|
This is satisfied by singularizing the prefix with the current datetime. If two
|
|
transactions are created simultaneously, `_compute_reference` ensures the uniqueness of
|
|
references by suffixing a sequence number.
|
|
|
|
:param str provider: The provider of the acquirer handling the transaction
|
|
:param str prefix: The custom prefix used to compute the full reference
|
|
:param str separator: The custom separator used to separate the prefix from the suffix
|
|
:return: The unique reference for the transaction
|
|
:rtype: str
|
|
"""
|
|
if provider == 'sips':
|
|
# We use an empty separator for cosmetic reasons: As the default prefix is 'tx', we want
|
|
# the singularized prefix to look like 'tx2020...' and not 'txx2020...'.
|
|
prefix = payment_utils.singularize_reference_prefix(separator='')
|
|
separator = 'x' # Still, we need a dedicated separator between the prefix and the seq.
|
|
return super()._compute_reference(provider, prefix=prefix, separator=separator, **kwargs)
|
|
|
|
def _get_specific_rendering_values(self, processing_values):
|
|
""" Override of payment to return Sips-specific rendering values.
|
|
|
|
Note: self.ensure_one() from `_get_processing_values`
|
|
|
|
:param dict processing_values: The generic and specific processing values of the transaction
|
|
:return: The dict of acquirer-specific processing values
|
|
:rtype: dict
|
|
"""
|
|
res = super()._get_specific_rendering_values(processing_values)
|
|
if self.provider != 'sips':
|
|
return res
|
|
|
|
base_url = self.get_base_url()
|
|
data = {
|
|
'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
|
|
'currencyCode': SUPPORTED_CURRENCIES[self.currency_id.name], # The ISO 4217 code
|
|
'merchantId': self.acquirer_id.sips_merchant_id,
|
|
'normalReturnUrl': urls.url_join(base_url, SipsController._return_url),
|
|
'automaticResponseUrl': urls.url_join(base_url, SipsController._webhook_url),
|
|
'transactionReference': self.reference,
|
|
'statementReference': self.reference,
|
|
'keyVersion': self.acquirer_id.sips_key_version,
|
|
'returnContext': json.dumps(dict(reference=self.reference)),
|
|
}
|
|
api_url = self.acquirer_id.sips_prod_url if self.acquirer_id.state == 'enabled' \
|
|
else self.acquirer_id.sips_test_url
|
|
data = '|'.join([f'{k}={v}' for k, v in data.items()])
|
|
return {
|
|
'api_url': api_url,
|
|
'Data': data,
|
|
'InterfaceVersion': self.acquirer_id.sips_version,
|
|
'Seal': self.acquirer_id._sips_generate_shasign(data),
|
|
}
|
|
|
|
@api.model
|
|
def _get_tx_from_feedback_data(self, provider, 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
|
|
: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':
|
|
return tx
|
|
|
|
data = self._sips_data_to_object(data['Data'])
|
|
reference = data.get('transactionReference')
|
|
|
|
if not reference:
|
|
return_context = json.loads(data.get('returnContext', '{}'))
|
|
reference = return_context.get('reference')
|
|
|
|
tx = self.search([('reference', '=', reference), ('provider', '=', 'sips')])
|
|
if not tx:
|
|
raise ValidationError(
|
|
"Sips: " + _("No transaction found matching reference %s.", reference)
|
|
)
|
|
|
|
return tx
|
|
|
|
def _process_feedback_data(self, 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
|
|
:return: None
|
|
"""
|
|
super()._process_feedback_data(data)
|
|
if self.provider != 'sips':
|
|
return
|
|
|
|
data = self._sips_data_to_object(data.get('Data'))
|
|
self.acquirer_reference = data.get('transactionReference')
|
|
response_code = data.get('responseCode')
|
|
if response_code in RESPONSE_CODES_MAPPING['pending']:
|
|
status = "pending"
|
|
self._set_pending()
|
|
elif response_code in RESPONSE_CODES_MAPPING['done']:
|
|
status = "done"
|
|
self._set_done()
|
|
elif response_code in RESPONSE_CODES_MAPPING['cancel']:
|
|
status = "cancel"
|
|
self._set_canceled()
|
|
else:
|
|
status = "error"
|
|
self._set_error(_("Unrecognized response received from the payment provider."))
|
|
_logger.info(
|
|
"received data with response %(response)s for transaction with reference %(ref)s, set "
|
|
"status as '%(status)s'",
|
|
{
|
|
'response': response_code,
|
|
'ref': self.reference,
|
|
'status': status,
|
|
},
|
|
)
|
|
|
|
def _sips_data_to_object(self, data):
|
|
res = {}
|
|
for element in data.split('|'):
|
|
key, value = element.split('=')
|
|
res[key] = value
|
|
return res
|