309 lines
14 KiB
Python
309 lines
14 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import pprint
|
|
|
|
from odoo import _, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
from odoo.addons.payment import utils as payment_utils
|
|
from odoo.addons.payment_authorize.models.authorize_request import AuthorizeAPI
|
|
from odoo.addons.payment_authorize.const import PAYMENT_METHODS_MAPPING, TRANSACTION_STATUS_MAPPING
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentTransaction(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
def _get_specific_processing_values(self, processing_values):
|
|
""" Override of payment to return an access token as provider-specific processing values.
|
|
|
|
Note: self.ensure_one() from `_get_processing_values`
|
|
|
|
:param dict processing_values: The generic processing values of the transaction
|
|
:return: The dict of provider-specific processing values
|
|
:rtype: dict
|
|
"""
|
|
res = super()._get_specific_processing_values(processing_values)
|
|
if self.provider_code != 'authorize':
|
|
return res
|
|
|
|
return {
|
|
'access_token': payment_utils.generate_access_token(
|
|
processing_values['reference'], processing_values['partner_id']
|
|
)
|
|
}
|
|
|
|
def _authorize_create_transaction_request(self, opaque_data):
|
|
""" Create an Authorize.Net payment transaction request.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param dict opaque_data: The payment details obfuscated by Authorize.Net
|
|
:return:
|
|
"""
|
|
self.ensure_one()
|
|
|
|
authorize_API = AuthorizeAPI(self.provider_id)
|
|
if self.provider_id.capture_manually or self.operation == 'validation':
|
|
return authorize_API.authorize(self, opaque_data=opaque_data)
|
|
else:
|
|
return authorize_API.auth_and_capture(self, opaque_data=opaque_data)
|
|
|
|
def _send_payment_request(self):
|
|
""" Override of payment to send a payment request to Authorize.
|
|
|
|
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 != 'authorize':
|
|
return
|
|
|
|
if not self.token_id.authorize_profile:
|
|
raise UserError("Authorize.Net: " + _("The transaction is not linked to a token."))
|
|
|
|
authorize_API = AuthorizeAPI(self.provider_id)
|
|
if self.provider_id.capture_manually:
|
|
res_content = authorize_API.authorize(self, token=self.token_id)
|
|
_logger.info(
|
|
"authorize request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(res_content)
|
|
)
|
|
else:
|
|
res_content = authorize_API.auth_and_capture(self, token=self.token_id)
|
|
_logger.info(
|
|
"auth_and_capture request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(res_content)
|
|
)
|
|
self._handle_notification_data('authorize', {'response': res_content})
|
|
|
|
def _send_refund_request(self, amount_to_refund=None):
|
|
""" Override of payment to send a refund request to Authorize.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param float amount_to_refund: The amount to refund
|
|
:return: The refund transaction created to process the refund request.
|
|
:rtype: recordset of `payment.transaction`
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if self.provider_code != 'authorize':
|
|
return super()._send_refund_request(amount_to_refund=amount_to_refund)
|
|
|
|
authorize_api = AuthorizeAPI(self.provider_id)
|
|
tx_details = authorize_api.get_transaction_details(self.provider_reference)
|
|
if 'err_code' in tx_details: # Could not retrieve the transaction details.
|
|
raise ValidationError("Authorize.Net: " + _(
|
|
"Could not retrieve the transaction details. (error code: %(error_code)s; error_details: %(error_message)s)",
|
|
error_code=tx_details['err_code'], error_message=tx_details.get('err_msg'),
|
|
))
|
|
|
|
refund_tx = self.env['payment.transaction']
|
|
tx_status = tx_details.get('transaction', {}).get('transactionStatus')
|
|
if tx_status in TRANSACTION_STATUS_MAPPING['voided']:
|
|
# The payment has been voided from Authorize.net side before we could refund it.
|
|
self._set_canceled(extra_allowed_states=('done',))
|
|
elif tx_status in TRANSACTION_STATUS_MAPPING['refunded']:
|
|
# The payment has been refunded from Authorize.net side before we could refund it. We
|
|
# create a refund tx on Odoo to reflect the move of the funds.
|
|
refund_tx = super()._send_refund_request(amount_to_refund=amount_to_refund)
|
|
refund_tx._set_done()
|
|
# Immediately post-process the transaction as the post-processing will not be
|
|
# triggered by a customer browsing the transaction from the portal.
|
|
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
|
|
elif any(tx_status in TRANSACTION_STATUS_MAPPING[k] for k in ('authorized', 'captured')):
|
|
if tx_status in TRANSACTION_STATUS_MAPPING['authorized']:
|
|
# The payment has not been settled on Authorize.net yet. It must be voided rather
|
|
# than refunded. Since the funds have not moved yet, we don't create a refund tx.
|
|
res_content = authorize_api.void(self.provider_reference)
|
|
tx_to_process = self
|
|
else:
|
|
# The payment has been settled on Authorize.net side. We can refund it.
|
|
refund_tx = super()._send_refund_request(amount_to_refund=amount_to_refund)
|
|
rounded_amount = round(amount_to_refund, self.currency_id.decimal_places)
|
|
res_content = authorize_api.refund(
|
|
self.provider_reference, rounded_amount, tx_details
|
|
)
|
|
tx_to_process = refund_tx
|
|
_logger.info(
|
|
"refund request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(res_content)
|
|
)
|
|
data = {'reference': tx_to_process.reference, 'response': res_content}
|
|
tx_to_process._handle_notification_data('authorize', data)
|
|
else:
|
|
raise ValidationError("Authorize.net: " + _(
|
|
"The transaction is not in a status to be refunded. (status: %(status)s, details: %(message)s)",
|
|
status=tx_status, message=tx_details.get('messages', {}).get('message'),
|
|
))
|
|
return refund_tx
|
|
|
|
def _send_capture_request(self, amount_to_capture=None):
|
|
""" Override of `payment` to send a capture request to Authorize. """
|
|
child_capture_tx = super()._send_capture_request(amount_to_capture=amount_to_capture)
|
|
if self.provider_code != 'authorize':
|
|
return child_capture_tx
|
|
|
|
authorize_API = AuthorizeAPI(self.provider_id)
|
|
rounded_amount = round(self.amount, self.currency_id.decimal_places)
|
|
res_content = authorize_API.capture(self.provider_reference, rounded_amount)
|
|
_logger.info(
|
|
"capture request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(res_content)
|
|
)
|
|
self._handle_notification_data('authorize', {'response': res_content})
|
|
|
|
return child_capture_tx
|
|
|
|
def _send_void_request(self, amount_to_void=None):
|
|
""" Override of payment to send a void request to Authorize. """
|
|
child_void_tx = super()._send_void_request(amount_to_void=amount_to_void)
|
|
if self.provider_code != 'authorize':
|
|
return child_void_tx
|
|
|
|
authorize_API = AuthorizeAPI(self.provider_id)
|
|
res_content = authorize_API.void(self.provider_reference)
|
|
_logger.info(
|
|
"void request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(res_content)
|
|
)
|
|
self._handle_notification_data('authorize', {'response': res_content})
|
|
|
|
return child_void_tx
|
|
|
|
def _get_tx_from_notification_data(self, provider_code, notification_data):
|
|
""" Find the transaction based on Authorize.net 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`
|
|
"""
|
|
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
|
|
if provider_code != 'authorize' or len(tx) == 1:
|
|
return tx
|
|
|
|
reference = notification_data.get('reference')
|
|
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'authorize')])
|
|
if not tx:
|
|
raise ValidationError(
|
|
"Authorize.Net: " + _("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 Authorize 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 != 'authorize':
|
|
return
|
|
|
|
response_content = notification_data.get('response')
|
|
|
|
# Update the provider reference.
|
|
self.provider_reference = response_content.get('x_trans_id')
|
|
|
|
# Update the payment method.
|
|
payment_method_code = response_content.get('payment_method_code', '').lower()
|
|
payment_method = self.env['payment.method']._get_from_code(
|
|
payment_method_code, mapping=PAYMENT_METHODS_MAPPING
|
|
)
|
|
self.payment_method_id = payment_method or self.payment_method_id
|
|
|
|
# Update the payment state.
|
|
status_code = response_content.get('x_response_code', '3')
|
|
if status_code == '1': # Approved
|
|
status_type = response_content.get('x_type').lower()
|
|
if status_type in ('auth_capture', 'prior_auth_capture'):
|
|
self._set_done()
|
|
if self.tokenize and not self.token_id:
|
|
self._authorize_tokenize()
|
|
elif status_type == 'auth_only':
|
|
self._set_authorized()
|
|
if self.tokenize and not self.token_id:
|
|
self._authorize_tokenize()
|
|
if self.operation == 'validation':
|
|
self._send_void_request() # In last step because it processes the response.
|
|
elif status_type == 'void':
|
|
if self.operation == 'validation': # Validation txs are authorized and then voided
|
|
self._set_done() # If the refund went through, the validation tx is confirmed
|
|
else:
|
|
self._set_canceled(extra_allowed_states=('done',))
|
|
elif status_type == 'refund' and self.operation == 'refund':
|
|
self._set_done()
|
|
# Immediately post-process the transaction as the post-processing will not be
|
|
# triggered by a customer browsing the transaction from the portal.
|
|
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
|
|
elif status_code == '2': # Declined
|
|
self._set_canceled(state_message=response_content.get('x_response_reason_text'))
|
|
elif status_code == '4': # Held for Review
|
|
self._set_pending()
|
|
else: # Error / Unknown code
|
|
error_code = response_content.get('x_response_reason_text')
|
|
_logger.info(
|
|
"received data with invalid status (%(status)s) and error code (%(err)s) for "
|
|
"transaction with reference %(ref)s",
|
|
{
|
|
'status': status_code,
|
|
'err': error_code,
|
|
'ref': self.reference,
|
|
},
|
|
)
|
|
self._set_error(
|
|
"Authorize.Net: " + _(
|
|
"Received data with status code \"%(status)s\" and error code \"%(error)s\"",
|
|
status=status_code, error=error_code
|
|
)
|
|
)
|
|
|
|
def _authorize_tokenize(self):
|
|
""" Create a token for the current transaction.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
self.ensure_one()
|
|
|
|
authorize_API = AuthorizeAPI(self.provider_id)
|
|
cust_profile = authorize_API.create_customer_profile(
|
|
self.partner_id, self.provider_reference
|
|
)
|
|
_logger.info(
|
|
"create_customer_profile request response for transaction with reference %s:\n%s",
|
|
self.reference, pprint.pformat(cust_profile)
|
|
)
|
|
if cust_profile:
|
|
token = self.env['payment.token'].create({
|
|
'provider_id': self.provider_id.id,
|
|
'payment_method_id': self.payment_method_id.id,
|
|
'payment_details': cust_profile.get('payment_details'),
|
|
'partner_id': self.partner_id.id,
|
|
'provider_ref': cust_profile.get('payment_profile_id'),
|
|
'authorize_profile': cust_profile.get('profile_id'),
|
|
})
|
|
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,
|
|
},
|
|
)
|