[REM] payment_ogone: remove the FlexCheckout API
Before this commit, the FlexCheckout API was used to process validation operations only. It proved itself to be: - unusable without making small payments with immediate refunds - badly suited to PSD2 due to its Merchant Initiated Transactions (MIT) - not worth the maintenance cost tied to its complexity given the simple flow that it implements - inconvenient to integrate to standard payment flows - poorly customizable in regard to the hosted tokenization page This commit thus removes it entirely and drops Ogone's support for validation operations with it. task-2494916
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import pprint
|
||||
import re
|
||||
|
||||
import markupsafe
|
||||
import werkzeug
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request
|
||||
from odoo.tools import ustr
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OgoneController(http.Controller):
|
||||
_hosted_payment_page_return_url = '/payment/ogone/hostedpaymentpage'
|
||||
_flexcheckout_return_url = '/payment/ogone/flexcheckout'
|
||||
_directlink_return_url = '/payment/ogone/directlink'
|
||||
_return_url = '/payment/ogone/return'
|
||||
_backward_compatibility_urls = [
|
||||
'/payment/ogone/accept', '/payment/ogone/test/accept',
|
||||
'/payment/ogone/decline', '/payment/ogone/test/decline',
|
||||
@@ -31,84 +26,11 @@ class OgoneController(http.Controller):
|
||||
] # Facilitates the migration of users who registered the URLs in Ogone's backend prior to 14.3
|
||||
|
||||
@http.route(
|
||||
_hosted_payment_page_return_url, type='http', auth='public', methods=['GET', 'POST'],
|
||||
csrf=False
|
||||
) # 'GET' or 'POST' depending on the configuration in Ogone backend
|
||||
def ogone_return_from_hosted_payment_page(self, **feedback_data):
|
||||
""" Process the data returned by Ogone after redirection to the Hosted Payment Page.
|
||||
|
||||
:param dict feedback_data: The feedback data
|
||||
"""
|
||||
# Check the source and integrity of the data
|
||||
data = self._homogenize_data(feedback_data)
|
||||
self._verify_signature(feedback_data, data)
|
||||
|
||||
# Handle the feedback data
|
||||
data['FEEDBACK_TYPE'] = 'hosted_payment_page'
|
||||
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data))
|
||||
request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
|
||||
return werkzeug.utils.redirect('/payment/status')
|
||||
|
||||
@http.route(
|
||||
_flexcheckout_return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False
|
||||
) # 'GET' or 'POST' depending on the configuration in Ogone backend
|
||||
def ogone_return_from_flexcheckout(self, **feedback_data):
|
||||
""" Process the data returned by Ogone after redirection to Flexcheckout.
|
||||
|
||||
:param dict feedback_data: The feedback data
|
||||
"""
|
||||
# Check the source and integrity of the data
|
||||
data = self._homogenize_data(feedback_data)
|
||||
self._verify_signature(feedback_data, data)
|
||||
|
||||
# Create a token from the feedback data
|
||||
data['FEEDBACK_TYPE'] = 'flexcheckout'
|
||||
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data))
|
||||
tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
|
||||
|
||||
# Process the payment through the token
|
||||
tree = tx_sudo._ogone_send_order_request(request_3ds_authentication=True)
|
||||
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)
|
||||
)
|
||||
request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', feedback_data)
|
||||
|
||||
# Handle the response
|
||||
redirect_html_element = tree.find('HTML_ANSWER')
|
||||
if redirect_html_element:
|
||||
# Ogone has inserted an HTML_ANSWER element in its response XML tree. This means that a
|
||||
# redirection to DirectLink's authentication page is required, as FlexCheckout is not
|
||||
# capable of handling authentications...
|
||||
# As per the documentation, the HTML must be inserted as-is into the current page, and
|
||||
# consists of a <form> bundled with a script to auto-submit the form once its inserted.
|
||||
# The content of the HTML is not sanitized to preserve the redirection in the script.
|
||||
# After redirection, the customer comes back to the directlink return URL and we proceed
|
||||
# with the (now authenticated) payment request.
|
||||
redirect_html = markupsafe.Markup(ustr(base64.b64decode(redirect_html_element.text)))
|
||||
return request.render(
|
||||
'payment_ogone.directlink_feedback', {'redirect_html': redirect_html}
|
||||
)
|
||||
else:
|
||||
if tx_sudo.state in ('cancel', 'error'):
|
||||
tx_sudo.token_id.active = False # The initial payment failed, archive the token
|
||||
return request.redirect('/payment/status')
|
||||
|
||||
@http.route(
|
||||
[_directlink_return_url] + _backward_compatibility_urls, type='http', auth='public',
|
||||
[_return_url] + _backward_compatibility_urls, type='http', auth='public',
|
||||
methods=['GET', 'POST'], csrf=False
|
||||
) # 'GET' or 'POST' depending on the configuration in Ogone backend
|
||||
def ogone_return_from_directlink(self, **feedback_data):
|
||||
""" Process the data returned by Ogone after redirection to Directlink authentication page.
|
||||
|
||||
A redirection to Directlink can happen if a 3DS1 authentication is requested when sending
|
||||
the request for a new order. This should normally only happen for the first payment of a
|
||||
token as this is the only case where we specifically request the authentication if necessary
|
||||
and handle the redirection request if one is returned.
|
||||
def ogone_return_from_redirect(self, **feedback_data):
|
||||
""" Process the data returned by Ogone after redirection to the Hosted Payment Page.
|
||||
|
||||
This route can also accept S2S notifications from Ogone if it is configured as a webhook in
|
||||
Ogone's backend.
|
||||
@@ -120,11 +42,8 @@ class OgoneController(http.Controller):
|
||||
self._verify_signature(feedback_data, data)
|
||||
|
||||
# Handle the feedback data
|
||||
data['FEEDBACK_TYPE'] = 'directlink'
|
||||
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data))
|
||||
tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
|
||||
if tx_sudo.state in ('cancel', 'error'):
|
||||
tx_sudo.token_id.active = False # The initial payment failed, archive the token
|
||||
request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
|
||||
return request.redirect('/payment/status')
|
||||
|
||||
def _homogenize_data(self, data):
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# See https://epayments-support.ingenico.com/en/integration/all-sales-channels/flexcheckout/guide#flexcheckout_integration_guides_sha_out
|
||||
FLEXCHECKOUT_KEYS = [
|
||||
'ALIAS.ALIASID',
|
||||
'ALIAS.NCERROR',
|
||||
'ALIAS.NCERRORCARDNO',
|
||||
'ALIAS.NCERRORCN',
|
||||
'ALIAS.NCERRORCVC',
|
||||
'ALIAS.NCERRORED',
|
||||
'ALIAS.ORDERID',
|
||||
'ALIAS.STATUS',
|
||||
'ALIAS.STOREPERMANENTLY',
|
||||
'CARD.BIC',
|
||||
'CARD.BIN',
|
||||
'CARD.BRAND',
|
||||
'CARD.CARDHOLDERNAME',
|
||||
'CARD.CARDNUMBER',
|
||||
'CARD.CVC',
|
||||
'CARD.EXPIRYDATE'
|
||||
]
|
||||
# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_request_a_new_order
|
||||
# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_order_response
|
||||
DIRECTLINK_KEYS = [
|
||||
VALID_KEYS = [
|
||||
'AAVADDRESS',
|
||||
'AAVCHECK',
|
||||
'AAVMAIL',
|
||||
@@ -86,7 +67,6 @@ DIRECTLINK_KEYS = [
|
||||
'TRXDATE',
|
||||
'VC',
|
||||
]
|
||||
VALID_KEYS = DIRECTLINK_KEYS + FLEXCHECKOUT_KEYS
|
||||
|
||||
|
||||
# See https://epayments-support.ingenico.com/en/get-started/transaction-status-full/
|
||||
|
||||
@@ -5,7 +5,7 @@ from hashlib import sha1
|
||||
|
||||
import requests
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from .const import VALID_KEYS
|
||||
@@ -31,37 +31,22 @@ class PaymentAcquirer(models.Model):
|
||||
ogone_shakey_out = fields.Char(
|
||||
string="SHA Key OUT", size=32, required_if_provider='ogone', groups='base.group_system')
|
||||
|
||||
def _get_validation_amount(self):
|
||||
""" Override of payment to return the amount for Ogone validation operations.
|
||||
@api.model
|
||||
def _get_compatible_acquirers(self, *args, is_validation=False, **kwargs):
|
||||
""" Override of payment to unlist Ogone acquirers for validation operations. """
|
||||
acquirers = super()._get_compatible_acquirers(*args, is_validation=is_validation, **kwargs)
|
||||
|
||||
:return: The validation amount
|
||||
:rtype: float
|
||||
"""
|
||||
res = super()._get_validation_amount()
|
||||
if self.provider != 'ogone':
|
||||
return res
|
||||
if is_validation:
|
||||
acquirers = acquirers.filtered(lambda a: a.provider != 'ogone')
|
||||
|
||||
return 1.0
|
||||
|
||||
def _get_redirect_form_view(self, is_validation=False):
|
||||
""" Override of payment to return the FlexCheckout form for validation operations.
|
||||
|
||||
:param bool is_validation: Whether the operation is a validation
|
||||
:return: The redirect form template
|
||||
:rtype: record of `ir.ui.view`
|
||||
"""
|
||||
res = super()._get_redirect_form_view()
|
||||
if self.provider != 'ogone' or not is_validation:
|
||||
return res # self.redirect_form_view_id
|
||||
|
||||
return self.env.ref('payment_ogone.redirect_form_validation')
|
||||
return acquirers
|
||||
|
||||
def _ogone_get_api_url(self, api_key):
|
||||
""" Return the appropriate URL of the requested API for the acquirer state.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param str api_key: The API whose URL to get: 'flexcheckout' or 'directlink'
|
||||
:param str api_key: The API whose URL to get: 'hosted_payment_page' or 'directlink'
|
||||
:return: The API URL
|
||||
:rtype: str
|
||||
"""
|
||||
@@ -70,16 +55,12 @@ class PaymentAcquirer(models.Model):
|
||||
if self.state == 'enabled':
|
||||
api_urls = {
|
||||
'hosted_payment_page': 'https://secure.ogone.com/ncol/prod/orderstandard_utf8.asp',
|
||||
'flexcheckout': 'https://secure.ogone.com/Tokenization/HostedPage',
|
||||
'directlink': 'https://secure.ogone.com/ncol/prod/orderdirect_utf8.asp',
|
||||
'maintenancedirect': 'https://secure.ogone.com/ncol/prod/maintenancedirect_utf8.asp',
|
||||
}
|
||||
else: # 'test'
|
||||
api_urls = {
|
||||
'hosted_payment_page': 'https://ogone.test.v-psp.com/ncol/test/orderstandard_utf8.asp',
|
||||
'flexcheckout': 'https://ogone.test.v-psp.com/Tokenization/HostedPage',
|
||||
'directlink': 'https://ogone.test.v-psp.com/ncol/test/orderdirect_utf8.asp',
|
||||
'maintenancedirect': 'https://ogone.test.v-psp.com/ncol/test/maintenancedirect_utf8.asp',
|
||||
}
|
||||
return api_urls.get(api_key)
|
||||
|
||||
@@ -110,12 +91,11 @@ class PaymentAcquirer(models.Model):
|
||||
shasign = sha1(signing_string.encode()).hexdigest()
|
||||
return shasign
|
||||
|
||||
def _ogone_make_request(self, api_key, payload=None, method='POST'):
|
||||
def _ogone_make_request(self, payload=None, method='POST'):
|
||||
""" Make a request to one of Ogone APIs.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param str api_key: The API to which the request is made: 'flexcheckout' or 'directlink'
|
||||
:param dict payload: The payload of the request
|
||||
:param str method: The HTTP method of the request
|
||||
:return The content of the response
|
||||
@@ -124,7 +104,7 @@ class PaymentAcquirer(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
url = self._ogone_get_api_url(api_key)
|
||||
url = self._ogone_get_api_url('directlink')
|
||||
try:
|
||||
response = requests.request(method, url, data=payload, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -61,57 +61,37 @@ class PaymentTransaction(models.Model):
|
||||
if self.acquirer_id.provider != 'ogone':
|
||||
return res
|
||||
|
||||
base_url = self.acquirer_id.get_base_url()
|
||||
if self.operation == 'online_redirect':
|
||||
return_url = urls.url_join(base_url, OgoneController._hosted_payment_page_return_url)
|
||||
rendering_values = {
|
||||
'PSPID': self.acquirer_id.ogone_pspid,
|
||||
'ORDERID': self.reference,
|
||||
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
|
||||
'CURRENCY': self.currency_id.name,
|
||||
'LANGUAGE': self.partner_lang or 'en_US',
|
||||
'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
|
||||
'USERID': self.acquirer_id.ogone_userid,
|
||||
'ACCEPTURL': return_url,
|
||||
'DECLINEURL': return_url,
|
||||
'EXCEPTIONURL': return_url,
|
||||
'CANCELURL': return_url,
|
||||
}
|
||||
if self.tokenize:
|
||||
rendering_values.update({
|
||||
'ALIAS': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
|
||||
'ALIASUSAGE': _("Storing your payment details is necessary for future use."),
|
||||
})
|
||||
return_url = urls.url_join(self.acquirer_id.get_base_url(), OgoneController._return_url)
|
||||
rendering_values = {
|
||||
'PSPID': self.acquirer_id.ogone_pspid,
|
||||
'ORDERID': self.reference,
|
||||
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
|
||||
'CURRENCY': self.currency_id.name,
|
||||
'LANGUAGE': self.partner_lang or 'en_US',
|
||||
'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
|
||||
'USERID': self.acquirer_id.ogone_userid,
|
||||
'ACCEPTURL': return_url,
|
||||
'DECLINEURL': return_url,
|
||||
'EXCEPTIONURL': return_url,
|
||||
'CANCELURL': return_url,
|
||||
}
|
||||
if self.tokenize:
|
||||
rendering_values.update({
|
||||
'SHASIGN': self.acquirer_id._ogone_generate_signature(
|
||||
rendering_values, incoming=False
|
||||
).upper(),
|
||||
'api_url': self.acquirer_id._ogone_get_api_url('hosted_payment_page'),
|
||||
})
|
||||
else: # validation
|
||||
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'),
|
||||
'ALIAS': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
|
||||
'ALIASUSAGE': _("Storing your payment details is necessary for future use."),
|
||||
})
|
||||
rendering_values.update({
|
||||
'SHASIGN': self.acquirer_id._ogone_generate_signature(
|
||||
rendering_values, incoming=False
|
||||
).upper(),
|
||||
'api_url': self.acquirer_id._ogone_get_api_url('hosted_payment_page'),
|
||||
})
|
||||
return rendering_values
|
||||
|
||||
def _send_payment_request(self):
|
||||
@@ -129,25 +109,8 @@ class PaymentTransaction(models.Model):
|
||||
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
|
||||
"""
|
||||
# Make the payment request
|
||||
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,
|
||||
@@ -166,14 +129,8 @@ class PaymentTransaction(models.Model):
|
||||
'OPERATION': 'SAL', # direct sale
|
||||
# Alias Manager parameters
|
||||
'ALIAS': self.token_id.acquirer_ref,
|
||||
'ALIASPERSISTEDAFTERUSE': 'Y' if self.token_id.active else 'N',
|
||||
'ALIASPERSISTEDAFTERUSE': 'Y',
|
||||
'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)
|
||||
|
||||
@@ -181,16 +138,20 @@ class PaymentTransaction(models.Model):
|
||||
"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)
|
||||
response_content = self.acquirer_id._ogone_make_request(data)
|
||||
try:
|
||||
tree = objectify.fromstring(response_content)
|
||||
except etree.XMLSyntaxError:
|
||||
raise ValidationError("Ogone: " + "Received badly structured response from the API.")
|
||||
|
||||
# Handle the feedback data
|
||||
_logger.info(
|
||||
"received payment request response as an etree:\n%s",
|
||||
etree.tostring(tree, pretty_print=True, encoding='utf-8')
|
||||
)
|
||||
return tree
|
||||
feedback_data = {'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)
|
||||
|
||||
@api.model
|
||||
def _get_tx_from_feedback_data(self, provider, data):
|
||||
@@ -221,111 +182,49 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
: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._ogone_tokenize_from_feedback_data(
|
||||
data.get('CARDNUMBER'),
|
||||
data['ALIASID'],
|
||||
not self.tokenize, # Immediately archive the token if it was not requested
|
||||
)
|
||||
elif feedback_type in ('hosted_payment_page', 'directlink'):
|
||||
if 'tree' in data:
|
||||
data = data['tree']
|
||||
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']:
|
||||
has_token_data = 'ALIAS' in data
|
||||
if self.tokenize and has_token_data:
|
||||
self._ogone_tokenize_from_feedback_data(
|
||||
data.get('CARDNO'), data['ALIAS'], False
|
||||
)
|
||||
if self.token_id:
|
||||
self.token_id.verified = True # Validity of the token has been confirmed
|
||||
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)
|
||||
)
|
||||
else:
|
||||
raise ValidationError(
|
||||
"Ogone: " + _("Received feedback data with unknown type: %s", feedback_type)
|
||||
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']:
|
||||
has_token_data = 'ALIAS' in data
|
||||
if self.tokenize and has_token_data:
|
||||
self._ogone_tokenize_from_feedback_data(data)
|
||||
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 _ogone_tokenize_from_feedback_data(self, card_number, acquirer_ref, is_one_shot_payment):
|
||||
def _ogone_tokenize_from_feedback_data(self, data):
|
||||
""" Create a token from feedback data.
|
||||
|
||||
:param str card_number: The obfuscated number of the card
|
||||
:param str acquirer_ref: The acquirer reference of the payment
|
||||
:param bool is_one_shot_payment: Whether the token was created for a one-shot payment
|
||||
:param dict data: The feedback data sent by the provider
|
||||
:return: None
|
||||
"""
|
||||
token_name = card_number or payment_utils.build_token_name() # Not requested in the backend
|
||||
token_name = 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': acquirer_ref,
|
||||
'verified': False, # Set to True as soon as a payment is authorized
|
||||
'active': not is_one_shot_payment, # Archive immediately if used for one-shot payment
|
||||
'acquirer_ref': data['ALIAS'],
|
||||
'verified': True, # The payment is authorized, so the payment method is valid
|
||||
})
|
||||
self.write({
|
||||
'token_id': token.id,
|
||||
'tokenize': False,
|
||||
})
|
||||
|
||||
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')
|
||||
"created token with id %s for partner with id %s", token.id, self.partner_id.id
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -15,9 +15,11 @@ from ..controllers.main import OgoneController
|
||||
@tagged('post_install', '-at_install')
|
||||
class OgoneTest(OgoneCommon):
|
||||
|
||||
def test_validation_amount(self):
|
||||
""" Test the value of the validation amount. """
|
||||
self.assertEqual(self.ogone._get_validation_amount(), 1.0)
|
||||
def test_incompatibility_with_validation_operation(self):
|
||||
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
|
||||
self.company.id, self.partner.id, is_validation=True
|
||||
)
|
||||
self.assertNotIn(self.ogone, acquirers)
|
||||
|
||||
@freeze_time('2011-11-02 12:00:21') # Freeze time for consistent singularization behavior
|
||||
def test_reference_is_singularized(self):
|
||||
@@ -49,7 +51,7 @@ class OgoneTest(OgoneCommon):
|
||||
@freeze_time('2011-11-02 12:00:21') # Freeze time for consistent singularization behavior
|
||||
def test_redirect_form_values(self):
|
||||
""" Test the values of the redirect form inputs for online payments. """
|
||||
return_url = self._build_url(OgoneController._hosted_payment_page_return_url)
|
||||
return_url = self._build_url(OgoneController._return_url)
|
||||
expected_values = {
|
||||
'PSPID': self.ogone.ogone_pspid,
|
||||
'ORDERID': self.reference,
|
||||
@@ -94,38 +96,3 @@ class OgoneTest(OgoneCommon):
|
||||
value,
|
||||
f"received value {inputs[form_key]} for input {form_key} (expected {value})"
|
||||
)
|
||||
|
||||
@freeze_time('2011-11-02 12:00:21') # Freeze time for consistent singularization behavior
|
||||
def test_redirect_form_validation_values(self):
|
||||
""" Test the values of the redirect form inputs for validation. """
|
||||
return_url = self._build_url(OgoneController._flexcheckout_return_url)
|
||||
expected_values = {
|
||||
'ACCOUNT_PSPID': self.ogone.ogone_pspid,
|
||||
'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
|
||||
'ALIAS_ORDERID': self.reference,
|
||||
'ALIAS_STOREPERMANENTLY': 'N', # 'Y' if self.tokenize
|
||||
'CARD_PAYMENTMETHOD': 'CreditCard',
|
||||
'LAYOUT_LANGUAGE': self.partner.lang,
|
||||
'PARAMETERS_ACCEPTURL': return_url,
|
||||
'PARAMETERS_EXCEPTIONURL': return_url,
|
||||
}
|
||||
expected_values['SHASIGNATURE_SHASIGN'] = self.ogone._ogone_generate_signature(
|
||||
expected_values, incoming=False, format_keys=True
|
||||
).upper()
|
||||
|
||||
tx = self.create_transaction(flow='dummy', operation='validation')
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = tx._get_processing_values()
|
||||
|
||||
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
|
||||
|
||||
self.assertEqual(form_info['action'], 'https://ogone.test.v-psp.com/Tokenization/HostedPage')
|
||||
inputs = form_info['inputs']
|
||||
self.assertEqual(len(expected_values), len(inputs))
|
||||
for rendering_key, value in expected_values.items():
|
||||
form_key = rendering_key.replace('_', '.')
|
||||
self.assertEqual(
|
||||
inputs[form_key],
|
||||
value,
|
||||
f"received value {inputs[form_key]} for input {form_key} (expected {value})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user