196 lines
8.0 KiB
Python
196 lines
8.0 KiB
Python
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import json
|
||
|
import logging
|
||
|
import re
|
||
|
import requests
|
||
|
|
||
|
from odoo import _, api, fields, models
|
||
|
from odoo.exceptions import ValidationError
|
||
|
|
||
|
from odoo.addons.payment import utils as payment_utils
|
||
|
from odoo.addons.payment_adyen import const
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class PaymentProvider(models.Model):
|
||
|
_inherit = 'payment.provider'
|
||
|
|
||
|
code = fields.Selection(
|
||
|
selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'})
|
||
|
adyen_merchant_account = fields.Char(
|
||
|
string="Merchant Account",
|
||
|
help="The code of the merchant account to use with this provider",
|
||
|
required_if_provider='adyen', groups='base.group_system')
|
||
|
adyen_api_key = fields.Char(
|
||
|
string="API Key", help="The API key of the webservice user", required_if_provider='adyen',
|
||
|
groups='base.group_system')
|
||
|
adyen_client_key = fields.Char(
|
||
|
string="Client Key", help="The client key of the webservice user",
|
||
|
required_if_provider='adyen')
|
||
|
adyen_hmac_key = fields.Char(
|
||
|
string="HMAC Key", help="The HMAC key of the webhook", required_if_provider='adyen',
|
||
|
groups='base.group_system')
|
||
|
adyen_api_url_prefix = fields.Char(
|
||
|
string="API URL Prefix",
|
||
|
help="The base URL for the API endpoints",
|
||
|
required_if_provider='adyen',
|
||
|
)
|
||
|
|
||
|
#=== CRUD METHODS ===#
|
||
|
|
||
|
@api.model_create_multi
|
||
|
def create(self, values_list):
|
||
|
for values in values_list:
|
||
|
self._adyen_extract_prefix_from_api_url(values)
|
||
|
return super().create(values_list)
|
||
|
|
||
|
def write(self, values):
|
||
|
self._adyen_extract_prefix_from_api_url(values)
|
||
|
return super().write(values)
|
||
|
|
||
|
@api.model
|
||
|
def _adyen_extract_prefix_from_api_url(self, values):
|
||
|
""" Update the create or write values with the prefix extracted from the API URL.
|
||
|
|
||
|
:param dict values: The create or write values.
|
||
|
:return: None
|
||
|
"""
|
||
|
if values.get('adyen_api_url_prefix'): # Test if we're duplicating a provider.
|
||
|
values['adyen_api_url_prefix'] = re.sub(
|
||
|
r'(?:https://)?(\w+-\w+).*', r'\1', values['adyen_api_url_prefix']
|
||
|
)
|
||
|
|
||
|
#=== 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 == 'adyen').update({
|
||
|
'support_manual_capture': 'partial',
|
||
|
'support_refund': 'partial',
|
||
|
'support_tokenization': True,
|
||
|
})
|
||
|
|
||
|
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||
|
|
||
|
def _adyen_make_request(self, endpoint, endpoint_param=None, payload=None, method='POST', idempotency_key=None):
|
||
|
""" Make a request to Adyen API at the specified endpoint.
|
||
|
|
||
|
Note: self.ensure_one()
|
||
|
|
||
|
:param str endpoint: The endpoint to be reached by the request
|
||
|
:param str endpoint_param: A variable required by some endpoints which are interpolated with
|
||
|
it if provided. For example, the provider reference of the source
|
||
|
transaction for the '/payments/{}/refunds' endpoint.
|
||
|
: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
|
||
|
"""
|
||
|
|
||
|
def _build_url(prefix_, version_, endpoint_):
|
||
|
""" Build an API URL by appending the version and endpoint to a base URL.
|
||
|
|
||
|
The final URL follows this pattern: `<_base>/V<_version>/<_endpoint>`.
|
||
|
|
||
|
:param str prefix_: The API URL prefix of the account.
|
||
|
:param int version_: The version of the endpoint.
|
||
|
:param str endpoint_: The endpoint of the URL.
|
||
|
:return: The final URL.
|
||
|
:rtype: str
|
||
|
"""
|
||
|
prefix_ = prefix_.rstrip('/') # Remove potential trailing slash
|
||
|
endpoint_ = endpoint_.lstrip('/') # Remove potential leading slash
|
||
|
test_mode_ = self.state == 'test'
|
||
|
prefix_ = f'{prefix_}.adyen' if test_mode_ else f'{prefix_}-checkout-live.adyenpayments'
|
||
|
return f'https://{prefix_}.com/checkout/V{version_}/{endpoint_}'
|
||
|
|
||
|
self.ensure_one()
|
||
|
|
||
|
version = const.API_ENDPOINT_VERSIONS[endpoint]
|
||
|
endpoint = endpoint if not endpoint_param else endpoint.format(endpoint_param)
|
||
|
url = _build_url(self.adyen_api_url_prefix, version, endpoint)
|
||
|
headers = {'X-API-Key': self.adyen_api_key}
|
||
|
if method == 'POST' and idempotency_key:
|
||
|
headers['idempotency-key'] = idempotency_key
|
||
|
try:
|
||
|
response = requests.request(method, url, json=payload, headers=headers, timeout=60)
|
||
|
try:
|
||
|
response.raise_for_status()
|
||
|
except requests.exceptions.HTTPError:
|
||
|
_logger.exception(
|
||
|
"invalid API request at %s with data %s: %s", url, payload, response.text
|
||
|
)
|
||
|
msg = response.json().get('message', '')
|
||
|
raise ValidationError(
|
||
|
"Adyen: " + _("The communication with the API failed. Details: %s", msg)
|
||
|
)
|
||
|
except requests.exceptions.ConnectionError:
|
||
|
_logger.exception("unable to reach endpoint at %s", url)
|
||
|
raise ValidationError("Adyen: " + _("Could not establish the connection to the API."))
|
||
|
return response.json()
|
||
|
|
||
|
def _adyen_compute_shopper_reference(self, partner_id):
|
||
|
""" Compute a unique reference of the partner for Adyen.
|
||
|
|
||
|
This is used for the `shopperReference` field in communications with Adyen and stored in the
|
||
|
`adyen_shopper_reference` field on `payment.token` if the payment method is tokenized.
|
||
|
|
||
|
:param recordset partner_id: The partner making the transaction, as a `res.partner` id
|
||
|
:return: The unique reference for the partner
|
||
|
:rtype: str
|
||
|
"""
|
||
|
return f'ODOO_PARTNER_{partner_id}'
|
||
|
|
||
|
#=== BUSINESS METHODS - GETTERS ===#
|
||
|
|
||
|
def _adyen_get_inline_form_values(self, pm_code, amount=None, currency=None):
|
||
|
""" Return a serialized JSON of the required values to render the inline form.
|
||
|
|
||
|
Note: `self.ensure_one()`
|
||
|
|
||
|
:param str pm_code: The code of the payment method whose inline form to render.
|
||
|
:param float amount: The transaction amount.
|
||
|
:param res.currency currency: The transaction currency.
|
||
|
:return: The JSON serial of the required values to render the inline form.
|
||
|
:rtype: str
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
|
||
|
inline_form_values = {
|
||
|
'client_key': self.adyen_client_key,
|
||
|
'adyen_pm_code': const.PAYMENT_METHODS_MAPPING.get(pm_code, pm_code),
|
||
|
'formatted_amount': self._adyen_get_formatted_amount(amount, currency),
|
||
|
}
|
||
|
return json.dumps(inline_form_values)
|
||
|
|
||
|
def _adyen_get_formatted_amount(self, amount=None, currency=None):
|
||
|
""" Return the amount in the format required by Adyen.
|
||
|
|
||
|
The formatted amount is a dict with keys 'value' and 'currency'.
|
||
|
|
||
|
:param float amount: The transaction amount.
|
||
|
:param res.currency currency: The transaction currency.
|
||
|
:return: The Adyen-formatted amount.
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
currency_code = currency and currency.name
|
||
|
converted_amount = amount and currency_code and payment_utils.to_minor_currency_units(
|
||
|
amount, currency, const.CURRENCY_DECIMALS.get(currency_code)
|
||
|
)
|
||
|
return {
|
||
|
'value': converted_amount,
|
||
|
'currency': currency_code,
|
||
|
}
|
||
|
|
||
|
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 != 'adyen':
|
||
|
return default_codes
|
||
|
return const.DEFAULT_PAYMENT_METHOD_CODES
|