# Part of Odoo. See LICENSE file for full copyright and licensing details. import hmac import logging import pprint from werkzeug.exceptions import Forbidden from odoo import SUPERUSER_ID, _ from odoo.http import Controller, request, route _logger = logging.getLogger(__name__) class GelatoController(Controller): _webhook_url = '/gelato/webhook' @route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False) def gelato_webhook(self): """ Process the notification data sent by Gelato to the webhook. See https://dashboard.gelato.com/docs/orders/order_details/#order-statuses for the event codes. :return: An empty response to acknowledge the notification. :rtype: odoo.http.Response """ event_data = request.get_json_data() _logger.info("Webhook notification received from Gelato:\n%s", pprint.pformat(event_data)) if event_data['event'] == 'order_status_updated': # Check the signature of the webhook notification. order_id = int(event_data['orderReferenceId']) order_sudo = request.env['sale.order'].sudo().browse(order_id).exists() received_signature = request.httprequest.headers.get('signature', '') self._verify_notification_signature(received_signature, order_sudo) # Process the event. fulfillment_status = event_data.get('fulfillmentStatus') if fulfillment_status == 'failed': # Log a message on the order. log_message = _( "Gelato could not proceed with the fulfillment of order %(order_reference)s:" " %(gelato_message)s", order_reference=order_sudo.display_name, gelato_message=event_data['comment'], ) order_sudo.message_post( body=log_message, author_id=request.env.ref('base.partner_root').id ) elif fulfillment_status == 'canceled': # Cancel the order. order_sudo.with_user(SUPERUSER_ID)._action_cancel() # Manually cache the currency while in a sudoed environment to prevent an # AccessError. The state of the sales order is a dependency of # `untaxed_amount_to_invoice`, which is a monetary field. They require the currency # to ensure the values are saved in the correct format. However, the currency cannot # be read directly during the flush due to access rights, necessitating manual # caching. order_sudo.order_line.currency_id # Send the generic order cancellation email. order_sudo.message_post_with_source( source_ref=request.env.ref('sale.mail_template_sale_cancellation'), author_id=request.env.ref('base.partner_root').id, ) elif fulfillment_status == 'in_transit': # Send the Gelato order status update email. tracking_data = self._extract_tracking_data(item_data=event_data['items']) order_sudo.with_context({'tracking_data': tracking_data}).message_post_with_source( source_ref=request.env.ref('sale_gelato.order_status_update'), author_id=request.env.ref('base.partner_root').id, ) elif fulfillment_status == 'delivered': # Send the Gelato order status update email. order_sudo.with_context({'order_delivered': True}).message_post_with_source( source_ref=request.env.ref('sale_gelato.order_status_update'), author_id=request.env.ref('base.partner_root').id, ) elif fulfillment_status == 'returned': # Log a message on the order. log_message = _( "Gelato has returned order %(reference)s.", reference=order_sudo.display_name ) order_sudo.message_post( body=log_message, author_id=request.env.ref('base.partner_root').id ) return request.make_json_response('') @staticmethod def _verify_notification_signature(received_signature, order_sudo): """ Check if the received signature matches the expected one. :param str received_signature: The received signature. :param sale.order order_sudo: The sales order for which the webhook notification was sent. :return: None :raise Forbidden: If the signatures don't match. """ company_sudo = order_sudo.company_id.sudo() # In sudo mode to read on the company. expected_signature = company_sudo.gelato_webhook_secret if not hmac.compare_digest(received_signature, expected_signature): _logger.warning("Received notification with invalid signature.") raise Forbidden() @staticmethod def _extract_tracking_data(item_data): """ Extract the tracking URL and code from the item data. :param dict item_data: The item data. :return: The extracted tracking data. :rtype: dict """ tracking_data = {} for i in item_data: for fulfilment_data in i['fulfillments']: tracking_data.setdefault( fulfilment_data['trackingUrl'], fulfilment_data['trackingCode'] ) # Different items can have the same tracking URL. return tracking_data