[IMP] payment_buckaroo: accept webhook notifications

Before this commit, it was not possible for a Buckaroo acquirer to
accept asynchronous notifications for payment updates. This is
important because, without them, an acquirer can only rely on
synchronous notifications (i.e. redirect requests from the provider's
checkout page) which can be unreliable and do not allow receiving
status updates, should payments take a bit longer to be confirmed by the
provider.

This commit adds a webhook controller whose route can be configured in
Buckaroo Plaza (backend) to send asynchronous notifications.

task-2687586

closes odoo/odoo#82922

Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Lucie Van Nieuwenhuyze <luvn@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv)
2022-01-31 17:47:39 +00:00
co-authored by Lucie Van Nieuwenhuyze
parent faeb5e7fee
commit 54fa480c2e
3 changed files with 63 additions and 7 deletions
+29 -1
View File
@@ -7,6 +7,7 @@ import pprint
from werkzeug.exceptions import Forbidden
from odoo import http
from odoo.exceptions import ValidationError
from odoo.http import request
_logger = logging.getLogger(__name__)
@@ -14,12 +15,13 @@ _logger = logging.getLogger(__name__)
class BuckarooController(http.Controller):
_return_url = '/payment/buckaroo/return'
_webhook_url = '/payment/buckaroo/webhook'
@http.route(
_return_url, type='http', auth='public', methods=['POST'], csrf=False, save_session=False
)
def buckaroo_return_from_checkout(self, **raw_data):
""" Process the notification data returned by Buckaroo after redirection from checkout.
""" Process the notification data sent by Buckaroo after redirection from checkout.
The route is flagged with `save_session=False` to prevent Odoo from assigning a new session
to the user if they are redirected to this route with a POST request. Indeed, as the session
@@ -45,6 +47,32 @@ class BuckarooController(http.Controller):
request.env['payment.transaction'].sudo()._handle_feedback_data('buckaroo', data)
return request.redirect('/payment/status')
@http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False)
def buckaroo_webhook(self, **raw_data):
""" Process the notification data sent by Buckaroo to the webhook.
See https://www.pronamic.nl/wp-content/uploads/2013/04/BPE-3.0-Gateway-HTML.1.02.pdf.
:param dict raw_data: The un-formatted notification data
:return: An empty string to acknowledge the notification
:rtype: str
"""
_logger.info("notification received from Buckaroo with data:\n%s", pprint.pformat(raw_data))
data = self._normalize_data_keys(raw_data)
try:
# Check the integrity of the notification
received_signature = data.get('brq_signature')
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
'buckaroo', data
)
self._verify_notification_signature(raw_data, received_signature, tx_sudo)
# Handle the notification data
request.env['payment.transaction'].sudo()._handle_feedback_data('buckaroo', data)
except ValidationError: # Acknowledge the notification to avoid getting spammed
_logger.exception("unable to handle the notification data; skipping to acknowledge")
return ''
@staticmethod
def _normalize_data_keys(data):
""" Set all keys of a dictionary to lower-case.
+13
View File
@@ -18,6 +18,19 @@ class BuckarooCommon(PaymentCommon):
'brq_signature': '5d389aa4f563cd99666a2e6bef79da3d4a32eb50',
}
ASYNC_NOTIFICATION_DATA = {
'brq_transactions': '0123456789ABCDEF0123456789ABCDEF',
'brq_transaction_method': 'paypal',
'brq_statuscode': '190', # confirmed
'brq_statusmessage': 'Transaction successfully processed',
'brq_invoicenumber': 'Test Transaction', # Shamefully copy-pasted from payment
'brq_amount': '1111.11',
'brq_currency': 'USD',
'brq_timestamp': '2022-01-01 12:00:00',
'brq_transaction_type': 'V010',
'brq_signature': '9ba976c3a6a3d2d1b5b58d3aa8c2c6fe269a9c27',
}
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
+21 -6
View File
@@ -1,16 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from werkzeug.exceptions import Forbidden
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.payment_buckaroo.controllers.main import BuckarooController
from odoo.addons.payment_buckaroo.tests.common import BuckarooCommon
@tagged('post_install', '-at_install')
class BuckarooTest(BuckarooCommon):
class BuckarooTest(BuckarooCommon, PaymentHttpCommon):
def test_redirect_form_values(self):
return_url = self._build_url(BuckarooController._return_url)
@@ -19,7 +22,7 @@ class BuckarooTest(BuckarooCommon):
'Brq_amount': str(self.amount),
'Brq_currency': self.currency.name,
'Brq_invoicenumber': self.reference,
'Brq_signature': '669d4f64ea9cbb58cfefba9b802389667e4eef39',
'Brq_signature': 'dacc220c3087edcc1200a38a6db0191c823e7f69',
'Brq_return': return_url,
'Brq_returncancel': return_url,
'Brq_returnerror': return_url,
@@ -58,14 +61,26 @@ class BuckarooTest(BuckarooCommon):
self.env['payment.transaction']._handle_feedback_data('buckaroo', notification_data)
self.assertEqual(tx.state, 'error')
@mute_logger('odoo.addons.payment_buckaroo.controllers.main')
def test_webhook_notification_confirms_transaction(self):
""" Test the processing of a webhook notification. """
tx = self.create_transaction('redirect')
url = self._build_url(BuckarooController._webhook_url)
with patch(
'odoo.addons.payment_buckaroo.controllers.main.BuckarooController'
'._verify_notification_signature'
):
self._make_http_post_request(url, data=self.ASYNC_NOTIFICATION_DATA)
self.assertEqual(tx.state, 'done')
def test_accept_notification_with_valid_signature(self):
""" Test the verification of a notification with a valid signature. """
tx = self.create_transaction('redirect')
self._assert_does_not_raise(
Forbidden,
BuckarooController._verify_notification_signature,
self.SYNC_NOTIFICATION_DATA,
self.SYNC_NOTIFICATION_DATA['brq_signature'],
self.ASYNC_NOTIFICATION_DATA,
self.ASYNC_NOTIFICATION_DATA['brq_signature'],
tx,
)
@@ -76,7 +91,7 @@ class BuckarooTest(BuckarooCommon):
self.assertRaises(
Forbidden,
BuckarooController._verify_notification_signature,
self.SYNC_NOTIFICATION_DATA,
self.ASYNC_NOTIFICATION_DATA,
None,
tx,
)
@@ -88,7 +103,7 @@ class BuckarooTest(BuckarooCommon):
self.assertRaises(
Forbidden,
BuckarooController._verify_notification_signature,
self.SYNC_NOTIFICATION_DATA,
self.ASYNC_NOTIFICATION_DATA,
'dummy',
tx,
)