Files
odoo_source/addons/payment_ogone/models/payment_transaction.py
T
Arnaud JosetandAntoine Vandevenne 61a02a7330 [REF] payment_ogone: migrate Ogone to the new payment API
In this commit, we take the opportunity to replace the previous unsecure
API with the new FlexCheckout API that implements payment methods
validation in a hosted page. Payment processing is still done with the
DirectLink API.

This commit also renames the module `payment_ingenico` to
`payment_ogone` as well as the referring strings ("Ogone" instead
of "Ingenico", ...).
A dedicated [MOV] commit is not used because neither the [MOV] commit
nor the adapted [REF] would be valid on its own.

See the merge commit for more details.

task-2333029
task-2313907
task-2334015

Co-authored-by: Antoine Vandevenne <anv@odoo.com>
2021-03-30 09:25:51 +02:00

285 lines
12 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from lxml import etree, objectify
from werkzeug import urls
from odoo import _, api, models
from odoo.exceptions import UserError, ValidationError
from . import const
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_ogone.controllers.main import OgoneController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
@api.model
def _compute_reference(self, provider, prefix=None, **kwargs):
""" Override of payment to ensure that Ogone requirements for references are satisfied.
Ogone requirements for references are as follows:
- 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
:return: The unique reference for the transaction
:rtype: str
"""
if provider != 'ogone':
return super()._compute_reference(provider, prefix=prefix, **kwargs)
prefix = payment_utils.singularize_reference_prefix(prefix=prefix, max_length=40)
return super()._compute_reference(provider, prefix=prefix, **kwargs)
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Ogone-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.acquirer_id.provider != 'ogone':
return res
base_url = self.acquirer_id._get_base_url()
return_url = urls.url_join(base_url, OgoneController._flexcheckout_return_url)
rendering_values = {
'ACCOUNT_PSPID': self.acquirer_id.ogone_pspid,
'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
'ALIAS_ORDERID': self.reference,
'ALIAS_STOREPERMANENTLY': 'Y' if self.tokenize else 'N',
'CARD_PAYMENTMETHOD': 'CreditCard',
'LAYOUT_LANGUAGE': self.partner_lang,
'PARAMETERS_ACCEPTURL': return_url,
'PARAMETERS_EXCEPTIONURL': return_url,
}
rendering_values.update({
'SHASIGNATURE_SHASIGN': self.acquirer_id._ogone_generate_signature(
rendering_values, incoming=False, format_keys=True
).upper(),
'api_url': self.acquirer_id._ogone_get_api_url('flexcheckout'),
})
return rendering_values
def _send_payment_request(self):
""" Override of payment to send a payment request to Ogone.
Note: self.ensure_one()
:return: None
:raise: UserError if the transaction is not linked to a token
"""
super()._send_payment_request()
if self.provider != 'ogone':
return
if not self.token_id:
raise UserError("Ogone: " + _("The transaction is not linked to a token."))
tree = self._ogone_send_order_request()
feedback_data = {
'FEEDBACK_TYPE': 'directlink',
'ORDERID': tree.get('orderID'),
'tree': tree,
}
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data))
self._handle_feedback_data('ogone', feedback_data)
def _ogone_send_order_request(self, request_3ds_authentication=False):
""" Make a new order request to Ogone and return the lxml etree parsed from the response.
:param bool request_3ds_authentication: Whether a 3DS authentication should be requested if
necessary to process the payment
:return: The lxml etree
:raise: ValidationError if the response can not be parsed to an lxml etree
"""
base_url = self.acquirer_id.get_base_url()
return_url = urls.url_join(base_url, OgoneController._directlink_return_url)
data = {
# DirectLink parameters
'PSPID': self.acquirer_id.ogone_pspid,
'ORDERID': self.reference,
'USERID': self.acquirer_id.ogone_userid,
'PSWD': self.acquirer_id.ogone_password,
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
'CURRENCY': self.currency_id.name,
'CN': self.partner_name or '', # Cardholder Name
'EMAIL': self.partner_email or '',
'OWNERADDRESS': self.partner_address or '',
'OWNERZIP': self.partner_zip or '',
'OWNERTOWN': self.partner_city or '',
'OWNERCTY': self.partner_country_id.code or '',
'OWNERTELNO': self.partner_phone or '',
'OPERATION': 'SAL', # direct sale
# Alias Manager parameters
'ALIAS': self.token_id.acquirer_ref,
'ALIASPERSISTEDAFTERUSE': 'Y' if self.token_id.active else 'N',
'ECI': 9, # Recurring (from eCommerce)
# 3DS parameters
'ACCEPTURL': return_url,
'DECLINEURL': return_url,
'EXCEPTIONURL': return_url,
'LANGUAGE': self.partner_lang or 'en_US',
'FLAG3D': 'Y' if request_3ds_authentication else 'N',
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False)
_logger.info(
"making payment request:\n%s",
pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
) # Log the payment request data without the password
response_content = self.acquirer_id._ogone_make_request('directlink', data)
try:
tree = objectify.fromstring(response_content)
except etree.XMLSyntaxError:
raise ValidationError("Ogone: " + "Received badly structured response from the API.")
_logger.info(
"received payment request response as an etree:\n%s",
etree.tostring(tree, pretty_print=True, encoding='utf-8')
)
return tree
@api.model
def _get_tx_from_feedback_data(self, provider, 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
: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':
return tx
reference = data.get('ORDERID')
tx = self.search([('reference', '=', reference), ('provider', '=', 'ogone')])
if not tx:
raise ValidationError(
"Ogone: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_feedback_data(self, 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
:return: None
:raise: ValidationError if inconsistent data were received
"""
super()._process_feedback_data(data)
if self.provider != 'ogone':
return
feedback_type = data.get('FEEDBACK_TYPE')
if feedback_type == 'flexcheckout':
self._process_flexcheckout_data(data)
elif feedback_type == 'directlink':
self._process_directlink_data(data)
else:
raise ValidationError(
"Ogone: " + _("Received feedback data with unknown type: %s", feedback_type)
)
def _process_flexcheckout_data(self, data):
""" Create a token from Flexcheckout feedback data.
:param dict data: The feedback data from Flexcheckout
:return: None
"""
token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id,
'name': data.get('CARDNUMBER'), # Already padded with 'X's
'partner_id': self.partner_id.id,
'acquirer_ref': data['ALIASID'],
'verified': False, # No payment has been processed through this token yet
'active': self.tokenize, # Immediately archive the token if it was not requested
})
self.write({
'token_id': token.id,
'tokenize': False,
})
def _process_directlink_data(self, data):
""" Update the transaction state and the acquirer reference based on the feedback data.
:param dict data: The feedback data from DirectLink
:return: None
"""
if 'tree' in data:
data = data['tree']
self.acquirer_reference = data.get('PAYID')
payment_status = int(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']:
self.token_id.verified = True # The payment has been authorized, the token is valid
self._set_done()
elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']:
self._set_canceled()
else: # Classify unknown payment statuses as `error` tx state
_logger.info("received data with invalid payment status: %s", payment_status)
self._set_error(
"Ogone: " + _("Received data with invalid payment status: %s", payment_status)
)
def _send_refund_request(self):
""" Override of payment to send a refund request to Authorize.
Note: self.ensure_one()
:return: None
:raise: ValidationError if a badly structured response is received
"""
super()._send_refund_request()
if self.provider != 'ogone':
return
data = {
'PSPID': self.acquirer_id.ogone_pspid,
'ORDERID': self.reference,
'PAYID': self.acquirer_reference,
'USERID': self.acquirer_id.ogone_userid,
'PSWD': self.acquirer_id.ogone_password,
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
'CURRENCY': self.currency_id.name,
'OPERATION': 'RFS', # refund
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False)
_logger.info(
"making refund request:\n%s",
pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
) # Log the refund request data without the password
response_content = self.acquirer_id._ogone_make_request('maintenancedirect', data)
try:
tree = objectify.fromstring(response_content)
except etree.XMLSyntaxError:
raise ValidationError("Ogone: " + "Received badly structured response from the API.")
_logger.info(
"received refund request response as an etree:\n%s",
etree.tostring(tree, pretty_print=True, encoding='utf-8')
)
feedback_data = {
'FEEDBACK_TYPE': 'directlink',
'ORDERID': tree.get('orderID'),
'tree': tree,
}
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data))
self._handle_feedback_data('ogone', feedback_data)