[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:
co-authored by
Lucie Van Nieuwenhuyze
parent
faeb5e7fee
commit
54fa480c2e
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user