diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py index 9bde082258f..069416a7e9e 100644 --- a/addons/payment_buckaroo/controllers/main.py +++ b/addons/payment_buckaroo/controllers/main.py @@ -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. diff --git a/addons/payment_buckaroo/tests/common.py b/addons/payment_buckaroo/tests/common.py index a0020e7ae53..a50147dd2f2 100644 --- a/addons/payment_buckaroo/tests/common.py +++ b/addons/payment_buckaroo/tests/common.py @@ -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) diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py index 62e1ce93b1d..ff6b1fd978c 100644 --- a/addons/payment_buckaroo/tests/test_buckaroo.py +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -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, )