[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:
committed by
Antoine Vandevenne (anv)
co-authored by
Antoine Vandevenne
parent
b1e730ed6e
commit
d9d2b6da6c
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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')]}"/>
|
||||
|
||||
Reference in New Issue
Block a user