File: main.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (129 lines) | stat: -rw-r--r-- 5,520 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import json
import logging
import pprint

from werkzeug.exceptions import Forbidden

from odoo import _, http
from odoo.exceptions import ValidationError
from odoo.http import request

from odoo.addons.payment_paypal import const


_logger = logging.getLogger(__name__)


class PaypalController(http.Controller):
    _complete_url = '/payment/paypal/complete_order'
    _webhook_url = '/payment/paypal/webhook/'

    @http.route(_complete_url, type='json', auth='public', methods=['POST'])
    def paypal_complete_order(self, provider_id, order_id):
        """ Make a capture request and handle the notification data.

        :param int provider_id: The provider handling the transaction, as a `payment.provider` id.
        :param string order_id: The order id provided by PayPal to identify the order.
        :return: None
        """
        provider_sudo = request.env['payment.provider'].browse(provider_id).sudo()
        response = provider_sudo._paypal_make_request(f'/v2/checkout/orders/{order_id}/capture')
        normalized_response = self._normalize_paypal_data(response)
        tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data(
            'paypal', normalized_response
        )
        tx_sudo._handle_notification_data('paypal', normalized_response)

    @http.route(_webhook_url, type='http', auth='public', methods=['POST'], csrf=False)
    def paypal_webhook(self):
        """ Process the notification data sent by PayPal to the webhook.

        See https://developer.paypal.com/docs/api/webhooks/v1/.

        :return: An empty string to acknowledge the notification.
        :rtype: str
        """
        data = request.get_json_data()
        if data.get('event_type') in const.HANDLED_WEBHOOK_EVENTS:
            normalized_data = self._normalize_paypal_data(
                data.get('resource'), from_webhook=True
            )
            _logger.info("Notification received from PayPal with data:\n%s", pprint.pformat(data))
            try:
                # Check the origin and integrity of the notification.
                tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data(
                    'paypal', normalized_data
                )
                self._verify_notification_origin(data, tx_sudo)

                # Handle the notification data.
                tx_sudo._handle_notification_data('paypal', normalized_data)
            except ValidationError:  # Acknowledge the notification to avoid getting spammed.
                _logger.warning(
                    "Unable to handle the notification data; skipping to acknowledge.",
                    exc_info=True,
                )
        return request.make_json_response('')

    def _normalize_paypal_data(self, data, from_webhook=False):
        """ Normalize the payment data received from PayPal.

        The payment data received from PayPal has a different format depending on whether the data
        come from the payment request response, or from the webhook.

        :param dict data: The data to normalize.
        :param bool from_webhook: Whether the data come from the webhook.
        :return: The normalized data.
        :rtype: dict
        """
        purchase_unit = data['purchase_units'][0]
        result = {
            'payment_source': data['payment_source'].keys(),
            'reference_id': purchase_unit.get('reference_id')
        }
        if from_webhook:
            result.update({
                **purchase_unit,
                'txn_type': data.get('intent'),
                'id': data.get('id'),
                'status': data.get('status'),
            })
        else:
            if captured := purchase_unit.get('payments', {}).get('captures'):
                result.update({
                    **captured[0],
                    'txn_type': 'CAPTURE',
                })
            else:
                raise ValidationError("PayPal: " + _("Invalid response format, can't normalize."))
        return result

    def _verify_notification_origin(self, notification_data, tx_sudo):
        """ Check that the notification was sent by PayPal.

        See https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post.

        :param dict notification_data: The notification data
        :param recordset tx_sudo: The sudoed transaction referenced in the notification data, as a
                                  `payment.transaction` record
        :return: None
        :raise Forbidden: If the notification origin can't be verified.
        """
        headers = request.httprequest.headers
        data = json.dumps({
            'transmission_id': headers.get('PAYPAL-TRANSMISSION-ID'),
            'transmission_time': headers.get('PAYPAL-TRANSMISSION-TIME'),
            'cert_url': headers.get('PAYPAL-CERT-URL'),
            'auth_algo': headers.get('PAYPAL-AUTH-ALGO'),
            'transmission_sig': headers.get('PAYPAL-TRANSMISSION-SIG'),
            'webhook_id': tx_sudo.provider_id.paypal_webhook_id,
            'webhook_event': notification_data,
        })
        verification = tx_sudo.provider_id._paypal_make_request(
            '/v1/notifications/verify-webhook-signature', data=data
        )
        if verification.get('verification_status') != 'SUCCESS':
            _logger.warning("Received notification that was not verified by PayPal.")
            raise Forbidden()