From d9d2b6da6c9d23f3f32f42728784dee632015fd5 Mon Sep 17 00:00:00 2001 From: Valentin Chevalier Date: Thu, 5 Aug 2021 09:06:06 +0000 Subject: [PATCH] [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) Co-authored-by: Antoine Vandevenne --- addons/payment_adyen/__manifest__.py | 5 +- addons/payment_adyen/const.py | 9 ++- addons/payment_adyen/controllers/main.py | 62 ++++++------------- .../payment_adyen/models/payment_acquirer.py | 11 +++- .../models/payment_transaction.py | 12 +--- .../static/src/js/payment_form.js | 53 +++++++--------- .../payment_adyen/static/src/scss/dropin.scss | 6 ++ addons/payment_adyen/tests/common.py | 1 + .../views/payment_adyen_templates.xml | 2 +- addons/payment_adyen/views/payment_views.xml | 1 + 10 files changed, 67 insertions(+), 95 deletions(-) create mode 100644 addons/payment_adyen/static/src/scss/dropin.scss diff --git a/addons/payment_adyen/__manifest__.py b/addons/payment_adyen/__manifest__.py index d298bf8916f..cd8449513f6 100644 --- a/addons/payment_adyen/__manifest__.py +++ b/addons/payment_adyen/__manifest__.py @@ -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', diff --git a/addons/payment_adyen/const.py b/addons/payment_adyen/const.py index 83325654431..9cac03fb2e6 100644 --- a/addons/payment_adyen/const.py +++ b/addons/payment_adyen/const.py @@ -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. diff --git a/addons/payment_adyen/controllers/main.py b/addons/payment_adyen/controllers/main.py index 85511e1e8dc..1dadf17c480 100644 --- a/addons/payment_adyen/controllers/main.py +++ b/addons/payment_adyen/controllers/main.py @@ -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 diff --git a/addons/payment_adyen/models/payment_acquirer.py b/addons/payment_adyen/models/payment_acquirer.py index 329dd4f82eb..4c6aba0402b 100644 --- a/addons/payment_adyen/models/payment_acquirer.py +++ b/addons/payment_adyen/models/payment_acquirer.py @@ -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() diff --git a/addons/payment_adyen/models/payment_transaction.py b/addons/payment_adyen/models/payment_transaction.py index e9bd9d4c0fd..cdf468717cc 100644 --- a/addons/payment_adyen/models/payment_transaction.py +++ b/addons/payment_adyen/models/payment_transaction.py @@ -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)", diff --git a/addons/payment_adyen/static/src/js/payment_form.js b/addons/payment_adyen/static/src/js/payment_form.js index 398bf6f3f35..0a91c61ba1e 100644 --- a/addons/payment_adyen/static/src/js/payment_form.js +++ b/addons/payment_adyen/static/src/js/payment_form.js @@ -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(); diff --git a/addons/payment_adyen/static/src/scss/dropin.scss b/addons/payment_adyen/static/src/scss/dropin.scss new file mode 100644 index 00000000000..4cfcfe4fb88 --- /dev/null +++ b/addons/payment_adyen/static/src/scss/dropin.scss @@ -0,0 +1,6 @@ +.o_adyen_dropin { + [aria-hidden="true"], [aria-hidden="1"] + { + display: block !important; + } +} diff --git a/addons/payment_adyen/tests/common.py b/addons/payment_adyen/tests/common.py index 45b2683d4b3..90e3e710104 100644 --- a/addons/payment_adyen/tests/common.py +++ b/addons/payment_adyen/tests/common.py @@ -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', diff --git a/addons/payment_adyen/views/payment_adyen_templates.xml b/addons/payment_adyen/views/payment_adyen_templates.xml index 5213c2deb16..6eb1c08e07d 100644 --- a/addons/payment_adyen/views/payment_adyen_templates.xml +++ b/addons/payment_adyen/views/payment_adyen_templates.xml @@ -2,7 +2,7 @@ diff --git a/addons/payment_adyen/views/payment_views.xml b/addons/payment_adyen/views/payment_views.xml index 876d360e9d7..78e511aeeff 100644 --- a/addons/payment_adyen/views/payment_views.xml +++ b/addons/payment_adyen/views/payment_views.xml @@ -10,6 +10,7 @@ +