payment_transaction.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import logging
  3. import pprint
  4. import uuid
  5. from lxml import etree, objectify
  6. from werkzeug import urls
  7. from odoo import _, api, models
  8. from odoo.exceptions import UserError, ValidationError
  9. from . import const
  10. from odoo.addons.payment import utils as payment_utils
  11. from odoo.addons.payment_ogone.controllers.main import OgoneController
  12. _logger = logging.getLogger(__name__)
  13. class PaymentTransaction(models.Model):
  14. _inherit = 'payment.transaction'
  15. @api.model
  16. def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs):
  17. """ Override of payment to ensure that Ogone requirements for references are satisfied.
  18. Ogone requirements for references are as follows:
  19. - References must be unique at provider level for a given merchant account.
  20. This is satisfied by singularizing the prefix with the current datetime. If two
  21. transactions are created simultaneously, `_compute_reference` ensures the uniqueness of
  22. references by suffixing a sequence number.
  23. :param str provider_code: The code of the provider handling the transaction
  24. :param str prefix: The custom prefix used to compute the full reference
  25. :param str separator: The custom separator used to separate the prefix from the suffix
  26. :return: The unique reference for the transaction
  27. :rtype: str
  28. """
  29. if provider_code != 'ogone':
  30. return super()._compute_reference(provider_code, prefix=prefix, **kwargs)
  31. if not prefix:
  32. # If no prefix is provided, it could mean that a module has passed a kwarg intended for
  33. # the `_compute_reference_prefix` method, as it is only called if the prefix is empty.
  34. # We call it manually here because singularizing the prefix would generate a default
  35. # value if it was empty, hence preventing the method from ever being called and the
  36. # transaction from received a reference named after the related document.
  37. prefix = self.sudo()._compute_reference_prefix(provider_code, separator, **kwargs) or None
  38. prefix = payment_utils.singularize_reference_prefix(prefix=prefix, max_length=40)
  39. return super()._compute_reference(provider_code, prefix=prefix, **kwargs)
  40. def _get_specific_rendering_values(self, processing_values):
  41. """ Override of payment to return Ogone-specific rendering values.
  42. Note: self.ensure_one() from `_get_processing_values`
  43. :param dict processing_values: The generic and specific processing values of the transaction
  44. :return: The dict of provider-specific processing values
  45. :rtype: dict
  46. """
  47. res = super()._get_specific_rendering_values(processing_values)
  48. if self.provider_code != 'ogone':
  49. return res
  50. return_url = urls.url_join(self.provider_id.get_base_url(), OgoneController._return_url)
  51. rendering_values = {
  52. 'PSPID': self.provider_id.ogone_pspid,
  53. 'ORDERID': self.reference,
  54. 'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
  55. 'CURRENCY': self.currency_id.name,
  56. 'LANGUAGE': self.partner_lang or 'en_US',
  57. 'EMAIL': self.partner_email or '',
  58. 'OWNERADDRESS': self.partner_address or '',
  59. 'OWNERZIP': self.partner_zip or '',
  60. 'OWNERTOWN': self.partner_city or '',
  61. 'OWNERCTY': self.partner_country_id.code or '',
  62. 'OWNERTELNO': self.partner_phone or '',
  63. 'OPERATION': 'SAL', # direct sale
  64. 'USERID': self.provider_id.ogone_userid,
  65. 'ACCEPTURL': return_url,
  66. 'DECLINEURL': return_url,
  67. 'EXCEPTIONURL': return_url,
  68. 'CANCELURL': return_url,
  69. }
  70. if self.tokenize:
  71. rendering_values.update({
  72. 'ALIAS': f'ODOO-ALIAS-{uuid.uuid4().hex}',
  73. 'ALIASUSAGE': _("Storing your payment details is necessary for future use."),
  74. })
  75. rendering_values.update({
  76. 'SHASIGN': self.provider_id._ogone_generate_signature(
  77. rendering_values, incoming=False
  78. ).upper(),
  79. 'api_url': self.provider_id._ogone_get_api_url('hosted_payment_page'),
  80. })
  81. return rendering_values
  82. def _send_payment_request(self):
  83. """ Override of payment to send a payment request to Ogone.
  84. Note: self.ensure_one()
  85. :return: None
  86. :raise: UserError if the transaction is not linked to a token
  87. """
  88. super()._send_payment_request()
  89. if self.provider_code != 'ogone':
  90. return
  91. if not self.token_id:
  92. raise UserError("Ogone: " + _("The transaction is not linked to a token."))
  93. # Make the payment request
  94. data = {
  95. # DirectLink parameters
  96. 'PSPID': self.provider_id.ogone_pspid,
  97. 'ORDERID': self.reference,
  98. 'USERID': self.provider_id.ogone_userid,
  99. 'PSWD': self.provider_id.ogone_password,
  100. 'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
  101. 'CURRENCY': self.currency_id.name,
  102. 'CN': self.partner_name or '', # Cardholder Name
  103. 'EMAIL': self.partner_email or '',
  104. 'OWNERADDRESS': self.partner_address or '',
  105. 'OWNERZIP': self.partner_zip or '',
  106. 'OWNERTOWN': self.partner_city or '',
  107. 'OWNERCTY': self.partner_country_id.code or '',
  108. 'OWNERTELNO': self.partner_phone or '',
  109. 'OPERATION': 'SAL', # direct sale
  110. # Alias Manager parameters
  111. 'ALIAS': self.token_id.provider_ref,
  112. 'ALIASPERSISTEDAFTERUSE': 'Y',
  113. 'ECI': 9, # Recurring (from eCommerce)
  114. }
  115. data['SHASIGN'] = self.provider_id._ogone_generate_signature(data, incoming=False)
  116. _logger.info(
  117. "payment request response for transaction with reference %s:\n%s",
  118. self.reference, pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
  119. ) # Log the payment request data without the password
  120. response_content = self.provider_id._ogone_make_request(data)
  121. try:
  122. tree = objectify.fromstring(response_content)
  123. except etree.XMLSyntaxError:
  124. raise ValidationError("Ogone: " + "Received badly structured response from the API.")
  125. # Handle the feedback data
  126. _logger.info(
  127. "payment request response (as an etree) for transaction with reference %s:\n%s",
  128. self.reference, etree.tostring(tree, pretty_print=True, encoding='utf-8')
  129. )
  130. feedback_data = {'ORDERID': tree.get('orderID'), 'tree': tree}
  131. _logger.info(
  132. "handling feedback data from Ogone for transaction with reference %s with data:\n%s",
  133. self.reference, pprint.pformat(feedback_data)
  134. )
  135. self._handle_notification_data('ogone', feedback_data)
  136. def _get_tx_from_notification_data(self, provider_code, notification_data):
  137. """ Override of payment to find the transaction based on Ogone data.
  138. :param str provider_code: The code of the provider that handled the transaction
  139. :param dict notification_data: The notification data sent by the provider
  140. :return: The transaction if found
  141. :rtype: recordset of `payment.transaction`
  142. :raise: ValidationError if the data match no transaction
  143. """
  144. tx = super()._get_tx_from_notification_data(provider_code, notification_data)
  145. if provider_code != 'ogone' or len(tx) == 1:
  146. return tx
  147. reference = notification_data.get('ORDERID')
  148. tx = self.search([('reference', '=', reference), ('provider_code', '=', 'ogone')])
  149. if not tx:
  150. raise ValidationError(
  151. "Ogone: " + _("No transaction found matching reference %s.", reference)
  152. )
  153. return tx
  154. def _process_notification_data(self, notification_data):
  155. """ Override of payment to process the transaction based on Ogone data.
  156. Note: self.ensure_one()
  157. :param dict notification_data: The notification data sent by the provider
  158. :return: None
  159. """
  160. super()._process_notification_data(notification_data)
  161. if self.provider_code != 'ogone':
  162. return
  163. if 'tree' in notification_data:
  164. notification_data = notification_data['tree']
  165. self.provider_reference = notification_data.get('PAYID')
  166. payment_status = int(notification_data.get('STATUS', '0'))
  167. if payment_status in const.PAYMENT_STATUS_MAPPING['pending']:
  168. self._set_pending()
  169. elif payment_status in const.PAYMENT_STATUS_MAPPING['done']:
  170. has_token_data = 'ALIAS' in notification_data
  171. if self.tokenize and has_token_data:
  172. self._ogone_tokenize_from_notification_data(notification_data)
  173. self._set_done()
  174. elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']:
  175. self._set_canceled()
  176. elif payment_status in const.PAYMENT_STATUS_MAPPING['declined']:
  177. if notification_data.get("NCERRORPLUS"):
  178. reason = notification_data.get("NCERRORPLUS")
  179. elif notification_data.get("NCERROR"):
  180. reason = "Error code: %s" % notification_data.get("NCERROR")
  181. else:
  182. reason = "Unknown reason"
  183. _logger.info("the payment has been declined: %s.", reason)
  184. self._set_error(
  185. "Ogone: " + _("The payment has been declined: %s", reason)
  186. )
  187. else: # Classify unknown payment statuses as `error` tx state
  188. _logger.info(
  189. "received data with invalid payment status (%s) for transaction with reference %s",
  190. payment_status, self.reference
  191. )
  192. self._set_error(
  193. "Ogone: " + _("Received data with invalid payment status: %s", payment_status)
  194. )
  195. def _ogone_tokenize_from_notification_data(self, notification_data):
  196. """ Create a token from notification data.
  197. :param dict notification_data: The notification data sent by the provider
  198. :return: None
  199. """
  200. token = self.env['payment.token'].create({
  201. 'provider_id': self.provider_id.id,
  202. 'payment_details': notification_data.get('CARDNO')[-4:], # Ogone pads details with X's.
  203. 'partner_id': self.partner_id.id,
  204. 'provider_ref': notification_data['ALIAS'],
  205. 'verified': True, # The payment is authorized, so the payment method is valid
  206. })
  207. self.write({
  208. 'token_id': token.id,
  209. 'tokenize': False,
  210. })
  211. _logger.info(
  212. "created token with id %(token_id)s for partner with id %(partner_id)s from "
  213. "transaction with reference %(ref)s",
  214. {
  215. 'token_id': token.id,
  216. 'partner_id': self.partner_id.id,
  217. 'ref': self.reference,
  218. },
  219. )