Odoo18-Base/addons/payment_ogone/models/payment_transaction.py

267 lines
12 KiB
Python
Raw Permalink Normal View History

2025-03-10 10:52:11 +07:00
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
import uuid
from lxml import etree, objectify
from werkzeug import urls
from odoo import _, api, models
from odoo.exceptions import UserError, ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_ogone import const
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_code, prefix=None, separator='-', **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_code: The code of the provider 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_code != 'ogone':
return super()._compute_reference(provider_code, prefix=prefix, **kwargs)
if not prefix:
# If no prefix is provided, it could mean that a module has passed a kwarg intended for
# the `_compute_reference_prefix` method, as it is only called if the prefix is empty.
# We call it manually here because singularizing the prefix would generate a default
# value if it was empty, hence preventing the method from ever being called and the
# transaction from received a reference named after the related document.
prefix = self.sudo()._compute_reference_prefix(provider_code, separator, **kwargs) or None
prefix = payment_utils.singularize_reference_prefix(prefix=prefix, max_length=40)
return super()._compute_reference(provider_code, 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 provider-specific processing values
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider_code != 'ogone':
return res
return_url = urls.url_join(self.provider_id.get_base_url(), OgoneController._return_url)
rendering_values = {
'PSPID': self.provider_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 '',
'CN': self.partner_name or '', # Cardholder Name
'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.provider_id.ogone_userid,
'ACCEPTURL': return_url,
'DECLINEURL': return_url,
'EXCEPTIONURL': return_url,
'CANCELURL': return_url,
'PM': const.PAYMENT_METHODS_MAPPING.get(
self.payment_method_code, self.payment_method_code
),
}
if self.tokenize:
rendering_values.update({
'ALIAS': f'ODOO-ALIAS-{uuid.uuid4().hex}',
'ALIASUSAGE': _("Storing your payment details is necessary for future use."),
})
rendering_values.update({
'SHASIGN': self.provider_id._ogone_generate_signature(
rendering_values, incoming=False
).upper(),
'api_url': self.provider_id._ogone_get_api_url('hosted_payment_page'),
})
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_code != 'ogone':
return
if not self.token_id:
raise UserError("Ogone: " + _("The transaction is not linked to a token."))
# Make the payment request
data = {
# DirectLink parameters
'PSPID': self.provider_id.ogone_pspid,
'ORDERID': self.reference,
'USERID': self.provider_id.ogone_userid,
'PSWD': self.provider_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.provider_ref,
'ALIASPERSISTEDAFTERUSE': 'Y',
'ECI': 9, # Recurring (from eCommerce)
}
data['SHASIGN'] = self.provider_id._ogone_generate_signature(data, incoming=False)
_logger.info(
"payment request response for transaction with reference %s:\n%s",
self.reference, pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
) # Log the payment request data without the password
response_content = self.provider_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(
"payment request response (as an etree) for transaction with reference %s:\n%s",
self.reference, etree.tostring(tree, pretty_print=True, encoding='utf-8')
)
feedback_data = {'ORDERID': tree.get('orderID'), 'tree': tree}
_logger.info(
"handling feedback data from Ogone for transaction with reference %s with data:\n%s",
self.reference, pprint.pformat(feedback_data)
)
self._handle_notification_data('ogone', feedback_data)
def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of payment to find the transaction based on Ogone data.
:param str provider_code: The code of the provider that handled the transaction
: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_notification_data(provider_code, notification_data)
if provider_code != 'ogone' or len(tx) == 1:
return tx
reference = notification_data.get('ORDERID')
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'ogone')])
if not tx:
raise ValidationError(
"Ogone: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_notification_data(self, notification_data):
""" Override of payment to process the transaction based on Ogone data.
Note: self.ensure_one()
:param dict notification_data: The notification data sent by the provider
:return: None
"""
super()._process_notification_data(notification_data)
if self.provider_code != 'ogone':
return
if 'tree' in notification_data:
notification_data = notification_data['tree']
# Update the provider reference.
self.provider_reference = notification_data.get('PAYID')
# Update the payment method.
payment_method_code = notification_data.get('BRAND', '')
payment_method = self.env['payment.method']._get_from_code(
payment_method_code, mapping=const.PAYMENT_METHODS_MAPPING
)
self.payment_method_id = payment_method or self.payment_method_id
# Update the payment state.
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 notification_data
if self.tokenize and has_token_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 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)
self._set_error(
"Ogone: " + _("The payment has been declined: %s", reason)
)
else: # Classify unknown payment statuses as `error` tx state
_logger.info(
"received data with invalid payment status (%s) for transaction with reference %s",
payment_status, self.reference
)
self._set_error(
"Ogone: " + _("Received data with invalid payment status: %s", payment_status)
)
def _ogone_tokenize_from_notification_data(self, notification_data):
""" Create a token from notification data.
:param dict notification_data: The notification data sent by the provider
:return: None
"""
token = self.env['payment.token'].create({
'provider_id': self.provider_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_details': notification_data.get('CARDNO')[-4:], # Ogone pads details with X's.
'partner_id': self.partner_id.id,
'provider_ref': notification_data['ALIAS'],
})
self.write({
'token_id': token.id,
'tokenize': False,
})
_logger.info(
"created token with id %(token_id)s for partner with id %(partner_id)s from "
"transaction with reference %(ref)s",
{
'token_id': token.id,
'partner_id': self.partner_id.id,
'ref': self.reference,
},
)