Odoo18-Base/addons/payment_worldline/models/payment_provider.py
2025-01-06 10:57:38 +07:00

146 lines
5.8 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import datetime
import hashlib
import hmac
import logging
import pprint
import requests
from odoo import _, fields, models
from odoo.exceptions import ValidationError
from odoo.addons.payment_worldline import const
_logger = logging.getLogger(__name__)
class PaymentProvider(models.Model):
_inherit = 'payment.provider'
code = fields.Selection(
selection_add=[('worldline', "Worldline")], ondelete={'worldline': 'set default'}
)
worldline_pspid = fields.Char(string="Worldline PSPID", required_if_provider='worldline')
worldline_api_key = fields.Char(string="Worldline API Key", required_if_provider='worldline')
worldline_api_secret = fields.Char(
string="Worldline API Secret", required_if_provider='worldline'
)
worldline_webhook_key = fields.Char(
string="Worldline Webhook Key", required_if_provider='worldline'
)
worldline_webhook_secret = fields.Char(
string="Worldline Webhook Secret", required_if_provider='worldline'
)
# === COMPUTE METHODS === #
def _compute_feature_support_fields(self):
""" Override of `payment` to enable additional features. """
super()._compute_feature_support_fields()
self.filtered(lambda p: p.code == 'worldline').update({
'support_tokenization': True,
})
# === BUSINESS METHODS === #
def _worldline_make_request(self, endpoint, payload=None, method='POST', idempotency_key=None):
""" Make a request to Worldline API at the specified endpoint.
Note: self.ensure_one()
:param str endpoint: The endpoint to be reached by the request.
:param dict payload: The payload of the request.
:param str method: The HTTP method of the request.
:param str idempotency_key: The idempotency key to pass in the request.
:return: The JSON-formatted content of the response.
:rtype: dict
:raise ValidationError: If an HTTP error occurs.
"""
self.ensure_one()
api_url = self._worldline_get_api_url()
url = f'{api_url}/v2/{self.worldline_pspid}/{endpoint}'
content_type = 'application/json; charset=utf-8' if method == 'POST' else ''
tz = datetime.timezone(datetime.timedelta(hours=0), 'GMT')
dt = datetime.datetime.now(tz).strftime('%a, %d %b %Y %H:%M:%S %Z') # Datetime in RFC1123.
signature = self._worldline_calculate_signature(
method, endpoint, content_type, dt, idempotency_key=idempotency_key
)
authorization_header = f'GCS v1HMAC:{self.worldline_api_key}:{signature}'
headers = {
'Authorization': authorization_header,
'Date': dt,
'Content-Type': content_type,
}
if method == 'POST' and idempotency_key:
headers['X-GCS-Idempotence-Key'] = idempotency_key
try:
response = requests.request(method, url, json=payload, headers=headers, timeout=10)
try:
response.raise_for_status()
except requests.exceptions.HTTPError:
_logger.exception(
"Invalid API request at %s with data:\n%s", url, pprint.pformat(payload)
)
msg = ', '.join(
[error.get('message', '') for error in response.json().get('errors', [])]
)
raise ValidationError(
"Worldline: " + _("The communication with the API failed. Details: %s", msg)
)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
_logger.exception("Unable to reach endpoint at %s", url)
raise ValidationError(
"Worldline: " + _("Could not establish the connection to the API.")
)
return response.json()
def _worldline_get_api_url(self):
""" Return the URL of the API corresponding to the provider's state.
:return: The API URL.
:rtype: str
"""
if self.state == 'enabled':
return 'https://payment.direct.worldline-solutions.com'
else: # 'test'
return 'https://payment.preprod.direct.worldline-solutions.com'
def _worldline_calculate_signature(
self, method, endpoint, content_type, dt_rfc, idempotency_key=None
):
""" Compute the signature for the provided data.
See https://docs.direct.worldline-solutions.com/en/integration/api-developer-guide/authentication.
:param str method: The HTTP method of the request
:param str endpoint: The endpoint to be reached by the request.
:param str content_type: The 'Content-Type' header of the request.
:param datetime.datetime dt_rfc: The timestamp of the request, in RFC1123 format.
:param str idempotency_key: The idempotency key to pass in the request.
:return: The calculated signature.
:rtype: str
"""
# specific order required: method, content_type, date, custom headers, endpoint
values_to_sign = [method, content_type, dt_rfc]
if idempotency_key:
values_to_sign.append(f'x-gcs-idempotence-key:{idempotency_key}')
values_to_sign.append(f'/v2/{self.worldline_pspid}/{endpoint}')
signing_str = '\n'.join(values_to_sign) + '\n'
signature = hmac.new(
self.worldline_api_secret.encode(), signing_str.encode(), hashlib.sha256
)
return base64.b64encode(signature.digest()).decode('utf-8')
def _get_default_payment_method_codes(self):
""" Override of `payment` to return the default payment method codes. """
default_codes = super()._get_default_payment_method_codes()
if self.code != 'worldline':
return default_codes
return const.DEFAULT_PAYMENT_METHOD_CODES