Skip to content

Commit 1f2af8a

Browse files
chevaliervAntoineVDV
authored andcommitted
[ADD] payment_adyen_paybylink: migrate from HPP to Pay by Link
Adyen announced the deprecation of their Hosted Payment Page (HPP) solution for the year 2022. They plan on stopping providing support on July 1st and on disabling the API on October 1st. This commit adds a new `payment_adyen_paybylink` module to migrate the current implementation of Adyen from the HPP API to the equivalent (with redirection) Pay by Link API. task-2607397 closes odoo#94651 X-original-commit: a323651 Signed-off-by: Antoine Vandevenne (anv) <[email protected]>
1 parent 7f84d50 commit 1f2af8a

File tree

12 files changed

+547
-2
lines changed

12 files changed

+547
-2
lines changed

.tx/config

+5-1
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ file_filter = addons/payment_adyen/i18n/<lang>.po
592592
source_file = addons/payment_adyen/i18n/payment_adyen.pot
593593
source_lang = en
594594

595+
[odoo-14.payment_adyen_paybylink]
596+
file_filter = addons/payment_adyen_paybylink/i18n/<lang>.po
597+
source_file = addons/payment_adyen_paybylink/i18n/payment_adyen_paybylink.pot
598+
source_lang = en
599+
595600
[odoo-14.payment_odoo_by_adyen]
596601
file_filter = addons/payment_odoo_by_adyen/i18n/<lang>.po
597602
source_file = addons/payment_odoo_by_adyen/i18n/payment_odoo_by_adyen.pot
@@ -1201,4 +1206,3 @@ source_lang = en
12011206
file_filter = addons/website_twitter/i18n/<lang>.po
12021207
source_file = addons/website_twitter/i18n/website_twitter.pot
12031208
source_lang = en
1204-

addons/payment/models/payment_acquirer.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ def render(self, reference, amount, currency_id, partner_id=False, values=None):
497497
values = method(values)
498498

499499
values.update({
500-
'tx_url': self._context.get('tx_url', self.get_form_action_url()),
500+
'tx_url': self._context.get(
501+
'tx_url', self.with_context(form_action_url_values=values).get_form_action_url()
502+
),
501503
'submit_class': self._context.get('submit_class', 'btn btn-link'),
502504
'submit_txt': self._context.get('submit_txt'),
503505
'acquirer': self,
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import api, SUPERUSER_ID
4+
5+
from . import models
6+
from . import controllers
7+
8+
def _post_init_hook(cr, registry):
9+
""" Disable the acquirer because new mandatory fields are added in this module. """
10+
env = api.Environment(cr, SUPERUSER_ID, {})
11+
acquirers = env['payment.acquirer'].search([
12+
('provider', '=', 'adyen'),
13+
('state', '!=', 'disabled'),
14+
])
15+
acquirers.write({
16+
'state': 'disabled',
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
{
3+
'name': "Adyen Payment Acquirer/Pay by Link Patch",
4+
'category': 'Accounting/Payment',
5+
'summary': "Payment Acquirer: Adyen Pay by Link Patch",
6+
'version': '1.0',
7+
'description': """
8+
This module migrates the Adyen implementation from the Hosted Payment Pages API to the Pay by Link
9+
API.
10+
""",
11+
'depends': ['payment_adyen'],
12+
'data': [
13+
'views/payment_views.xml',
14+
],
15+
'post_init_hook': '_post_init_hook',
16+
'auto_install': True,
17+
'license': 'LGPL-3'
18+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
# Endpoints of the API.
4+
# See https://docs.adyen.com/api-explorer/#/CheckoutService/v68/overview for Checkout API
5+
API_ENDPOINT_VERSIONS = {
6+
'/paymentLinks': 68, # Checkout API
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from . import main
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
import base64
4+
import binascii
5+
import hashlib
6+
import hmac
7+
import logging
8+
import pprint
9+
10+
from werkzeug.exceptions import Forbidden
11+
12+
from odoo.exceptions import ValidationError
13+
from odoo.http import request, route
14+
15+
from odoo.addons.payment_adyen.controllers.main import AdyenController
16+
17+
_logger = logging.getLogger(__name__)
18+
19+
20+
class AdyenPayByLinkController(AdyenController):
21+
22+
@route()
23+
def adyen_notification(self, **post):
24+
""" Process the data sent by Adyen to the webhook based on the event code.
25+
26+
See https://docs.adyen.com/development-resources/webhooks/understand-notifications for the
27+
exhaustive list of event codes.
28+
29+
:return: The '[accepted]' string to acknowledge the notification
30+
:rtype: str
31+
"""
32+
_logger.info(
33+
"notification received from Adyen with data:\n%s", pprint.pformat(post)
34+
)
35+
try:
36+
# Check the integrity of the notification
37+
tx_sudo = request.env['payment.transaction'].sudo()._adyen_form_get_tx_from_data(post)
38+
self._verify_notification_signature(post, tx_sudo)
39+
40+
# Check whether the event of the notification succeeded and reshape the notification
41+
# data for parsing
42+
event_code = post['eventCode']
43+
if event_code == 'AUTHORISATION' and post['success'] == 'true':
44+
post['authResult'] = 'AUTHORISED'
45+
46+
# Handle the notification data
47+
request.env['payment.transaction'].sudo().form_feedback(post, 'adyen')
48+
except ValidationError: # Acknowledge the notification to avoid getting spammed
49+
_logger.exception("unable to handle the notification data; skipping to acknowledge")
50+
51+
return '[accepted]' # Acknowledge the notification
52+
53+
@staticmethod
54+
def _verify_notification_signature(notification_data, tx_sudo):
55+
""" Check that the received signature matches the expected one.
56+
57+
:param dict notification_data: The notification payload containing the received signature
58+
:param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a
59+
`payment.transaction` record
60+
:return: None
61+
:raise: :class:`werkzeug.exceptions.Forbidden` if the signatures don't match
62+
"""
63+
# Retrieve the received signature from the payload
64+
received_signature = notification_data.get('additionalData.hmacSignature')
65+
if not received_signature:
66+
_logger.warning("received notification with missing signature")
67+
raise Forbidden()
68+
69+
# Compare the received signature with the expected signature computed from the payload
70+
hmac_key = tx_sudo.acquirer_id.adyen_hmac_key
71+
expected_signature = AdyenPayByLinkController._compute_signature(
72+
notification_data, hmac_key
73+
)
74+
if not hmac.compare_digest(received_signature, expected_signature):
75+
_logger.warning("received notification with invalid signature")
76+
raise Forbidden()
77+
78+
@staticmethod
79+
def _compute_signature(payload, hmac_key):
80+
""" Compute the signature from the payload.
81+
82+
See https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures
83+
84+
:param dict payload: The notification payload
85+
:param str hmac_key: The HMAC key of the acquirer handling the transaction
86+
:return: The computed signature
87+
:rtype: str
88+
"""
89+
def _flatten_dict(_value, _path_base='', _separator='.'):
90+
""" Recursively generate a flat representation of a dict.
91+
92+
:param Object _value: The value to flatten. A dict or an already flat value
93+
:param str _path_base: They base path for keys of _value, including preceding separators
94+
:param str _separator: The string to use as a separator in the key path
95+
"""
96+
if isinstance(_value, dict): # The inner value is a dict, flatten it
97+
_path_base = _path_base if not _path_base else _path_base + _separator
98+
for _key in _value:
99+
yield from _flatten_dict(_value[_key], _path_base + str(_key))
100+
else: # The inner value cannot be flattened, yield it
101+
yield _path_base, _value
102+
103+
def _to_escaped_string(_value):
104+
""" Escape payload values that are using illegal symbols and cast them to string.
105+
106+
String values containing `\\` or `:` are prefixed with `\\`.
107+
Empty values (`None`) are replaced by an empty string.
108+
109+
:param Object _value: The value to escape
110+
:return: The escaped value
111+
:rtype: string
112+
"""
113+
if isinstance(_value, str):
114+
return _value.replace('\\', '\\\\').replace(':', '\\:')
115+
elif _value is None:
116+
return ''
117+
else:
118+
return str(_value)
119+
120+
signature_keys = [
121+
'pspReference', 'originalReference', 'merchantAccountCode', 'merchantReference',
122+
'value', 'currency', 'eventCode', 'success'
123+
]
124+
# Build the list of signature values as per the list of required signature keys
125+
signature_values = [payload.get(key) for key in signature_keys]
126+
# Escape values using forbidden symbols
127+
escaped_values = [_to_escaped_string(value) for value in signature_values]
128+
# Concatenate values together with ':' as delimiter
129+
signing_string = ':'.join(escaped_values)
130+
# Convert the HMAC key to the binary representation
131+
binary_hmac_key = binascii.a2b_hex(hmac_key.encode('ascii'))
132+
# Calculate the HMAC with the binary representation of the signing string with SHA-256
133+
binary_hmac = hmac.new(binary_hmac_key, signing_string.encode('utf-8'), hashlib.sha256)
134+
# Calculate the signature by encoding the result with Base64
135+
return base64.b64encode(binary_hmac.digest()).decode()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
# * payment_adyen_paybylink
4+
#
5+
msgid ""
6+
msgstr ""
7+
"Project-Id-Version: Odoo Server 14.0\n"
8+
"Report-Msgid-Bugs-To: \n"
9+
"POT-Creation-Date: 2022-06-27 11:46+0000\n"
10+
"PO-Revision-Date: 2022-06-27 11:46+0000\n"
11+
"Last-Translator: \n"
12+
"Language-Team: \n"
13+
"MIME-Version: 1.0\n"
14+
"Content-Type: text/plain; charset=UTF-8\n"
15+
"Content-Transfer-Encoding: \n"
16+
"Plural-Forms: \n"
17+
18+
#. module: payment_adyen_paybylink
19+
#: code:addons/payment_adyen_paybylink/models/payment_transaction.py:0
20+
#, python-format
21+
msgid "; multiple order found"
22+
msgstr ""
23+
24+
#. module: payment_adyen_paybylink
25+
#: code:addons/payment_adyen_paybylink/models/payment_transaction.py:0
26+
#, python-format
27+
msgid "; no order found"
28+
msgstr ""
29+
30+
#. module: payment_adyen_paybylink
31+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__adyen_api_key
32+
msgid "API Key"
33+
msgstr ""
34+
35+
#. module: payment_adyen_paybylink
36+
#: code:addons/payment_adyen_paybylink/models/payment_transaction.py:0
37+
#, python-format
38+
msgid "Adyen: received data for reference %s"
39+
msgstr ""
40+
41+
#. module: payment_adyen_paybylink
42+
#: code:addons/payment_adyen_paybylink/models/payment_transaction.py:0
43+
#, python-format
44+
msgid ""
45+
"Adyen: received data with missing reference (%s) or missing pspReference "
46+
"(%s)"
47+
msgstr ""
48+
49+
#. module: payment_adyen_paybylink
50+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__adyen_checkout_api_url
51+
msgid "Checkout API URL"
52+
msgstr ""
53+
54+
#. module: payment_adyen_paybylink
55+
#: code:addons/payment_adyen_paybylink/models/payment_acquirer.py:0
56+
#, python-format
57+
msgid "Could not establish the connection to the API."
58+
msgstr ""
59+
60+
#. module: payment_adyen_paybylink
61+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__display_name
62+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_transaction__display_name
63+
msgid "Display Name"
64+
msgstr ""
65+
66+
#. module: payment_adyen_paybylink
67+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__adyen_hmac_key
68+
msgid "HMAC Key"
69+
msgstr ""
70+
71+
#. module: payment_adyen_paybylink
72+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__id
73+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_transaction__id
74+
msgid "ID"
75+
msgstr ""
76+
77+
#. module: payment_adyen_paybylink
78+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer____last_update
79+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_transaction____last_update
80+
msgid "Last Modified on"
81+
msgstr ""
82+
83+
#. module: payment_adyen_paybylink
84+
#: model:ir.model,name:payment_adyen_paybylink.model_payment_acquirer
85+
msgid "Payment Acquirer"
86+
msgstr ""
87+
88+
#. module: payment_adyen_paybylink
89+
#: model:ir.model,name:payment_adyen_paybylink.model_payment_transaction
90+
msgid "Payment Transaction"
91+
msgstr ""
92+
93+
#. module: payment_adyen_paybylink
94+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__adyen_skin_code
95+
msgid "Skin Code"
96+
msgstr ""
97+
98+
#. module: payment_adyen_paybylink
99+
#: model:ir.model.fields,field_description:payment_adyen_paybylink.field_payment_acquirer__adyen_skin_hmac_key
100+
msgid "Skin HMAC Key"
101+
msgstr ""
102+
103+
#. module: payment_adyen_paybylink
104+
#: model:ir.model.fields,help:payment_adyen_paybylink.field_payment_acquirer__adyen_api_key
105+
msgid "The API key of the webservice user"
106+
msgstr ""
107+
108+
#. module: payment_adyen_paybylink
109+
#: model:ir.model.fields,help:payment_adyen_paybylink.field_payment_acquirer__adyen_hmac_key
110+
msgid "The HMAC key of the webhook"
111+
msgstr ""
112+
113+
#. module: payment_adyen_paybylink
114+
#: model:ir.model.fields,help:payment_adyen_paybylink.field_payment_acquirer__adyen_checkout_api_url
115+
msgid "The base URL for the Checkout API endpoints"
116+
msgstr ""
117+
118+
#. module: payment_adyen_paybylink
119+
#: code:addons/payment_adyen_paybylink/models/payment_acquirer.py:0
120+
#, python-format
121+
msgid "The communication with the API failed."
122+
msgstr ""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from . import payment_acquirer
4+
from . import payment_transaction

0 commit comments

Comments
 (0)