[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>
This commit is contained in:
Valentin Chevalier
2021-09-03 14:30:18 +00:00
committed by Antoine Vandevenne (anv)
co-authored by Antoine Vandevenne
parent b1e730ed6e
commit d9d2b6da6c
10 changed files with 67 additions and 95 deletions
+3 -2
View File
@@ -17,9 +17,10 @@
'uninstall_hook': 'uninstall_hook',
'assets': {
'web.assets_frontend': [
'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/3.9.4/adyen.css',
'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/3.9.4/adyen.js',
'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/4.7.3/adyen.css',
'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/4.7.3/adyen.js',
'payment_adyen/static/src/js/payment_form.js',
'payment_adyen/static/src/scss/dropin.scss',
],
},
'license': 'LGPL-3',
+4 -5
View File
@@ -1,15 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Endpoints of the API.
# See https://docs.adyen.com/api-explorer/#/CheckoutService/v53/overview for Checkout API
# See https://docs.adyen.com/api-explorer/#/CheckoutService/v67/overview for Checkout API
# See https://docs.adyen.com/api-explorer/#/Recurring/v49/overview for Recurring API
API_ENDPOINT_VERSIONS = {
'/disable': 49, # Recurring API
'/originKeys': 53, # Checkout API
'/payments': 53, # Checkout API
'/payments/details': 53, # Checkout API
'/payments': 67, # Checkout API
'/payments/details': 67, # Checkout API
'/payments/{}/refunds': 67, # Checkout API
'/paymentMethods': 53, # Checkout API
'/paymentMethods': 67, # Checkout API
}
# Adyen-specific mapping of currency codes in ISO 4217 format to the number of decimals.
+19 -43
View File
@@ -24,16 +24,19 @@ _logger = logging.getLogger(__name__)
class AdyenController(http.Controller):
@http.route('/payment/adyen/acquirer_state', type='json', auth='public')
def adyen_acquirer_state(self, acquirer_id):
""" Return the state of the acquirer.
@http.route('/payment/adyen/acquirer_info', type='json', auth='public')
def adyen_acquirer_info(self, acquirer_id):
""" Return public information on the acquirer.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
:return: The state of the acquirer: 'enabled' or 'test'
:return: Public information on the acquirer, namely: the state and client key
:rtype: str
"""
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(acquirer_id).exists()
return acquirer_sudo.state
return {
'state': acquirer_sudo.state,
'client_key': acquirer_sudo.adyen_client_key,
}
@http.route('/payment/adyen/payment_methods', type='json', auth='public')
def adyen_payment_methods(self, acquirer_id, amount=None, currency_id=None, partner_id=None):
@@ -77,34 +80,12 @@ class AdyenController(http.Controller):
_logger.info("paymentMethods request response:\n%s", pprint.pformat(response_content))
return response_content
@http.route('/payment/adyen/origin_key', type='json', auth='public')
def adyen_origin_key(self, acquirer_id):
""" Request an origin key based on the current domain.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
:return: The JSON-formatted content of the response
:rtype: dict
"""
acquirer_sudo = request.env['payment.acquirer'].browse(acquirer_id).sudo()
domain = acquirer_sudo.get_base_url()
data = {
'originDomains': [domain],
}
response_content = acquirer_sudo._adyen_make_request(
url_field_name='adyen_checkout_api_url',
endpoint='/originKeys',
payload=data,
method='POST'
)
_logger.info("originKeys request response:\n%s", pprint.pformat(response_content))
return response_content
@http.route('/payment/adyen/payments', type='json', auth='public')
def adyen_payments(
self, acquirer_id, reference, converted_amount, currency_id, partner_id, payment_method,
access_token, browser_info=None
):
""" Make a payment request and handle the response.
""" Make a payment request and process the feedback data.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
:param str reference: The reference of the transaction
@@ -149,7 +130,7 @@ class AdyenController(http.Controller):
'returnUrl': urls.url_join(
acquirer_sudo.get_base_url(),
# Include the reference in the return url to be able to match it after redirection.
# The key 'merchantReference' is chosen on purpose to be the same than that returned
# The key 'merchantReference' is chosen on purpose to be the same as that returned
# by the /payments endpoint of Adyen.
f'/payment/adyen/return?merchantReference={reference}'
),
@@ -166,35 +147,27 @@ class AdyenController(http.Controller):
request.env['payment.transaction'].sudo()._handle_feedback_data(
'adyen', dict(response_content, merchantReference=reference), # Match the transaction
)
if 'action' in response_content and response_content['action']['type'] == 'redirect':
tx_sudo.adyen_payment_data = response_content['paymentData']
return response_content
@http.route('/payment/adyen/payment_details', type='json', auth='public')
def adyen_payment_details(self, acquirer_id, reference, details, payment_data):
""" Query the status of a transaction that required additional actions and process it.
def adyen_payment_details(self, acquirer_id, reference, payment_details):
""" Submit the details of the additional actions and process the feedback data.
The additional actions can have been performed both from the inline form or during a
redirection.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
:param str reference: The reference of the transaction
:param dict details: The specification of the additional actions
:param str payment_data: The encrypted payment data of the transaction
:param dict payment_details: The details of the additional actions performed for the payment
:return: The JSON-formatted content of the response
:rtype: dict
"""
# Make the payment details request to Adyen
acquirer_sudo = request.env['payment.acquirer'].browse(acquirer_id).sudo()
data = {
'details': details,
'paymentData': payment_data,
}
response_content = acquirer_sudo._adyen_make_request(
url_field_name='adyen_checkout_api_url',
endpoint='/payments/details',
payload=data,
payload=payment_details,
method='POST'
)
@@ -237,8 +210,11 @@ class AdyenController(http.Controller):
self.adyen_payment_details(
tx_sudo.acquirer_id.id,
data['merchantReference'],
{detail: value for detail, value in data.items() if detail != 'merchantReference'},
tx_sudo.adyen_payment_data,
{
'details': {
'redirectResult': data['redirectResult'],
},
},
)
# Redirect the user to the status page
@@ -23,8 +23,11 @@ class PaymentAcquirer(models.Model):
help="The code of the merchant account to use with this acquirer",
required_if_provider='adyen', groups='base.group_system')
adyen_api_key = fields.Char(
string="API Key", help="The API key of the user account", required_if_provider='adyen',
string="API Key", help="The API key of the webservice user", required_if_provider='adyen',
groups='base.group_system')
adyen_client_key = fields.Char(
string="Client Key", help="The client key of the webservice user",
required_if_provider='adyen')
adyen_hmac_key = fields.Char(
string="HMAC Key", help="The HMAC key of the webhook", required_if_provider='adyen',
groups='base.group_system')
@@ -107,8 +110,10 @@ class PaymentAcquirer(models.Model):
except requests.exceptions.ConnectionError:
_logger.exception("unable to reach endpoint at %s", url)
raise ValidationError("Adyen: " + _("Could not establish the connection to the API."))
except requests.exceptions.HTTPError:
_logger.exception("invalid API request at %s with data %s", url, payload)
except requests.exceptions.HTTPError as error:
_logger.exception(
"invalid API request at %s with data %s: %s", url, payload, error.response.text
)
raise ValidationError("Adyen: " + _("The communication with the API failed."))
return response.json()
@@ -15,11 +15,6 @@ _logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
adyen_payment_data = fields.Char(
string="Saved Payment Data",
help="Data that must be passed back to Adyen when returning from redirect", readonly=True,
groups='base.group_system')
#=== BUSINESS METHODS ===#
def _get_specific_processing_values(self, processing_values):
@@ -74,9 +69,8 @@ class PaymentTransaction(models.Model):
},
'reference': self.reference,
'paymentMethod': {
'recurringDetailReference': self.token_id.acquirer_ref
}, # Required by Adyen although it is also provided with 'storedPaymentMethodId'
'storedPaymentMethodId': self.token_id.acquirer_ref,
'recurringDetailReference': self.token_id.acquirer_ref,
},
'shopperReference': self.token_id.adyen_shopper_reference,
'recurringProcessingModel': 'Subscription',
'shopperIP': payment_utils.get_customer_ip_address(),
@@ -237,7 +231,7 @@ class PaymentTransaction(models.Model):
_logger.warning("An error occurred on transaction with reference %s (reason: %s)",
self.reference, refusal_reason)
self._set_error(
_("An error occured during processing of your payment. Please try again.")
_("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)",
@@ -29,8 +29,7 @@ odoo.define('payment_adyen.payment_form', require => {
params: {
'acquirer_id': dropin.acquirerId,
'reference': this.adyenDropin.reference,
'details': state.data.details,
'payment_data': state.data.paymentData,
'payment_details': state.data,
},
}).then(paymentDetails => {
if (paymentDetails.action) { // Additional action required from the shopper
@@ -133,13 +132,13 @@ odoo.define('payment_adyen.payment_form', require => {
// Overwrite the flow of the select payment option
this._setPaymentFlow('direct');
// Get the acquirer state ("enabled" or "test")
// Get public information on the acquirer (state, client_key)
return this._rpc({
route: '/payment/adyen/acquirer_state',
route: '/payment/adyen/acquirer_info',
params: {
'acquirer_id': paymentOptionId,
},
}).then(acquirerState => {
}).then(acquirerInfo => {
// Get the available payment methods
return this._rpc({
route: '/payment/adyen/payment_methods',
@@ -154,38 +153,28 @@ odoo.define('payment_adyen.payment_form', require => {
: undefined,
},
}).then(paymentMethodsResult => {
// Generate the origin key
return this._rpc({
route: '/payment/adyen/origin_key',
params: {
'acquirer_id': paymentOptionId,
},
}).then(originKeyResult => {
// Extract the origin key from the returned object
const originKeyResultKeys = Object.keys(originKeyResult.originKeys);
const originKey = originKeyResult.originKeys[originKeyResultKeys[0]];
// Instantiate the drop-in
const configuration = {
paymentMethodsResponse: paymentMethodsResult,
originKey: originKey,
locale: (this._getContext().lang || 'en-US').replace('_', '-'),
environment: acquirerState === 'enabled' ? 'live' : 'test',
// Instantiate the drop-in
const configuration = {
paymentMethodsResponse: paymentMethodsResult,
clientKey: acquirerInfo.client_key,
locale: (this._getContext().lang || 'en-US').replace('_', '-'),
environment: acquirerInfo.state === 'enabled' ? 'live' : 'test',
onAdditionalDetails: this._dropinOnAdditionalDetails.bind(this),
onError: this._dropinOnError.bind(this),
onSubmit: this._dropinOnSubmit.bind(this),
};
const checkout = new AdyenCheckout(configuration);
this.adyenDropin = checkout.create(
'dropin', {
openFirstPaymentMethod: true,
openFirstStoredPaymentMethod: false,
showStoredPaymentMethods: false,
showPaymentMethods: true,
showPayButton: false,
onAdditionalDetails: this._dropinOnAdditionalDetails.bind(this),
onError: this._dropinOnError.bind(this),
onSubmit: this._dropinOnSubmit.bind(this),
};
const checkout = new AdyenCheckout(configuration);
this.adyenDropin = checkout.create('dropin').mount(
`#o_adyen_dropin_container_${paymentOptionId}`
);
this.adyenDropin.acquirerId = paymentOptionId;
});
setStatusAutomatically: true,
},
).mount(`#o_adyen_dropin_container_${paymentOptionId}`);
this.adyenDropin.acquirerId = paymentOptionId;
});
}).guardedCatch((error) => {
error.event.preventDefault();
@@ -0,0 +1,6 @@
.o_adyen_dropin {
[aria-hidden="true"], [aria-hidden="1"]
{
display: block !important;
}
}
+1
View File
@@ -11,6 +11,7 @@ class AdyenCommon(PaymentCommon):
cls.adyen = cls._prepare_acquirer('adyen', update_values={
'adyen_merchant_account': 'dummy',
'adyen_api_key': 'dummy',
'adyen_client_key': 'dummy',
'adyen_hmac_key': 'dummy',
'adyen_checkout_api_url': 'https://this.is.an.url',
'adyen_recurring_api_url': 'https://this.is.an.url',
@@ -2,7 +2,7 @@
<odoo>
<template id="inline_form">
<div t-attf-id="o_adyen_dropin_container_{{acquirer_id}}"/>
<div t-attf-id="o_adyen_dropin_container_{{acquirer_id}}" class="o_adyen_dropin"/>
</template>
</odoo>
@@ -10,6 +10,7 @@
<group attrs="{'invisible': [('provider', '!=', 'adyen')]}">
<field name="adyen_merchant_account" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_api_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_client_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_hmac_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_checkout_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_recurring_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>