Files
odoo_source/addons/payment_adyen/models/payment_transaction.py
T
Valentin ChevalierandAntoine Vandevenne d9d2b6da6c [IMP] payment_adyen: migrate to the latest version of Checkout API
Checkout API v67
Web Drop-in v4.7.3

In particular, this commit changes the client-side authentication flow
to rely on client keys rather than origin keys as the latter is
deprecated by Adyen and the switch is required in order to upgrade the
Drop-in integration.

task-2590477

closes odoo/odoo#74827

Related: odoo/upgrade#2786
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Antoine Vandevenne <anv@odoo.com>
2021-09-03 14:30:18 +00:00

271 lines
11 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_adyen.const import CURRENCY_DECIMALS, RESULT_CODES_MAPPING
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
#=== BUSINESS METHODS ===#
def _get_specific_processing_values(self, processing_values):
""" Override of payment to return Adyen-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 acquirer-specific processing values
:rtype: dict
"""
res = super()._get_specific_processing_values(processing_values)
if self.provider != 'adyen':
return res
converted_amount = payment_utils.to_minor_currency_units(
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
)
return {
'converted_amount': converted_amount,
'access_token': payment_utils.generate_access_token(
processing_values['reference'],
converted_amount,
processing_values['partner_id']
)
}
def _send_payment_request(self):
""" Override of payment to send a payment request to Adyen.
Note: self.ensure_one()
:return: None
:raise: UserError if the transaction is not linked to a token
"""
super()._send_payment_request()
if self.provider != 'adyen':
return
# Make the payment request to Adyen
if not self.token_id:
raise UserError("Adyen: " + _("The transaction is not linked to a token."))
converted_amount = payment_utils.to_minor_currency_units(
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
)
data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account,
'amount': {
'value': converted_amount,
'currency': self.currency_id.name,
},
'reference': self.reference,
'paymentMethod': {
'recurringDetailReference': self.token_id.acquirer_ref,
},
'shopperReference': self.token_id.adyen_shopper_reference,
'recurringProcessingModel': 'Subscription',
'shopperIP': payment_utils.get_customer_ip_address(),
'shopperInteraction': 'ContAuth',
}
response_content = self.acquirer_id._adyen_make_request(
url_field_name='adyen_checkout_api_url',
endpoint='/payments',
payload=data,
method='POST'
)
# Handle the payment request response
_logger.info("payment request response:\n%s", pprint.pformat(response_content))
self._handle_feedback_data('adyen', response_content)
def _send_refund_request(self, refund_amount=None, create_refund_transaction=True):
""" Override of payment to send a refund request to Adyen.
Note: self.ensure_one()
:param float refund_amount: The amount to refund
:param bool create_refund_transaction: Whether a refund transaction should be created or not
:return: None
"""
if self.provider != 'adyen':
return super()._send_refund_request(
refund_amount=refund_amount, create_refund_transaction=create_refund_transaction
)
refund_tx = super()._send_refund_request(
refund_amount=refund_amount, create_refund_transaction=True
)
# Make the refund request to Adyen
converted_amount = payment_utils.to_minor_currency_units(
-refund_tx.amount, # The amount is negative for refund transactions
refund_tx.currency_id,
arbitrary_decimal_number=CURRENCY_DECIMALS.get(refund_tx.currency_id.name)
)
data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account,
'amount': {
'value': converted_amount,
'currency': refund_tx.currency_id.name,
},
'reference': refund_tx.reference,
}
response_content = refund_tx.acquirer_id._adyen_make_request(
url_field_name='adyen_checkout_api_url',
endpoint='/payments/{}/refunds',
endpoint_param=self.acquirer_reference,
payload=data,
method='POST'
)
_logger.info("refund request response:\n%s", pprint.pformat(response_content))
# Handle the refund request response
psp_reference = response_content.get('pspReference')
status = response_content.get('status')
if psp_reference and status == 'received':
# The PSP reference associated with this /refunds request is different from the psp
# reference associated with the original payment request.
refund_tx.acquirer_reference = psp_reference
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Override of payment to find the transaction based on Adyen 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 inconsistent data were received
:raise: ValidationError if the data match no transaction
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'adyen':
return tx
reference = data.get('merchantReference')
if not reference:
raise ValidationError("Adyen: " + _("Received data with missing merchant reference"))
event_code = data.get('eventCode')
tx = self.search([('reference', '=', reference), ('provider', '=', 'adyen')])
if event_code == 'REFUND' and tx and tx.operation != 'refund':
# If a refund is initiated from Adyen, the merchant reference in the feedback data is
# that of the source transaction. We need to manually create the refund transaction.
tx = self._adyen_create_refund_tx_from_feedback_data(tx, data)
if not tx:
raise ValidationError(
"Adyen: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _adyen_create_refund_tx_from_feedback_data(self, source_tx, data):
""" Create a refund transaction based on Adyen data.
:param recordset source_tx: The source transaction for which a refund is initiated, as a
`payment.transaction` recordset
:param dict data: The feedback data sent by the provider
:return: The created refund transaction
:rtype: recordset of `payment.transaction`
:raise: ValidationError if inconsistent data were received
"""
refund_acquirer_reference = data.get('pspReference')
refund_amount = data.get('amount', {}).get('value')
if not refund_acquirer_reference or not refund_amount:
raise ValidationError(
"Adyen: " + _("Received refund data with missing transaction values")
)
converted_amount = payment_utils.to_major_currency_units(
refund_amount, source_tx.currency_id
)
return source_tx._create_refund_transaction(
refund_amount=converted_amount, acquirer_reference=refund_acquirer_reference
)
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Adyen 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 != 'adyen':
return
# Handle the acquirer reference
if 'pspReference' in data:
self.acquirer_reference = data.get('pspReference')
# Handle the payment state
payment_state = data.get('resultCode')
refusal_reason = data.get('refusalReason') or data.get('reason')
if not payment_state:
raise ValidationError("Adyen: " + _("Received data with missing payment state."))
if payment_state in RESULT_CODES_MAPPING['pending']:
self._set_pending()
elif payment_state in RESULT_CODES_MAPPING['done']:
has_token_data = 'recurring.recurringDetailReference' in data.get('additionalData', {})
if self.tokenize and has_token_data:
self._adyen_tokenize_from_feedback_data(data)
self._set_done()
if self.operation == 'refund':
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
elif payment_state in RESULT_CODES_MAPPING['cancel']:
_logger.warning("The transaction with reference %s was cancelled (reason: %s)",
self.reference, refusal_reason)
self._set_canceled()
elif payment_state in RESULT_CODES_MAPPING['error']:
_logger.warning("An error occurred on transaction with reference %s (reason: %s)",
self.reference, refusal_reason)
self._set_error(
_("An error occurred during the processing of your payment. Please try again.")
)
elif payment_state in RESULT_CODES_MAPPING['refused']:
_logger.warning("The transaction with reference %s was refused (reason: %s)",
self.reference, refusal_reason)
self._set_error(_("Your payment was refused. Please try again."))
else: # Classify unsupported payment state as `error` tx state
_logger.warning("received data with invalid payment state: %s", payment_state)
self._set_error(
"Adyen: " + _("Received data with invalid payment state: %s", payment_state)
)
def _adyen_tokenize_from_feedback_data(self, data):
""" Create a new token based on the feedback data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
"""
self.ensure_one()
token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id,
'name': payment_utils.build_token_name(data['additionalData'].get('cardSummary')),
'partner_id': self.partner_id.id,
'acquirer_ref': data['additionalData']['recurring.recurringDetailReference'],
'adyen_shopper_reference': data['additionalData']['recurring.shopperReference'],
'verified': True, # The payment is authorized, so the payment method is valid
})
self.write({
'token_id': token,
'tokenize': False,
})
_logger.info(
"created token with id %s for partner with id %s", token.id, self.partner_id.id
)