Skip to content
Merged
6 changes: 5 additions & 1 deletion Adyen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from .client import AdyenClient
from .services import (
AdyenBase,
AdyenBinLookup,
AdyenRecurring,
AdyenPayment,
AdyenThirdPartyPayout,
AdyenHPP,
AdyenCheckoutApi)
AdyenCheckoutApi
)

from .httpclient import HTTPClient

Expand All @@ -28,6 +30,7 @@ class Adyen(AdyenBase):
def __init__(self, **kwargs):
self.client = AdyenClient(**kwargs)
self.payment = AdyenPayment(client=self.client)
self.binlookup = AdyenBinLookup(client=self.client)
self.payout = AdyenThirdPartyPayout(client=self.client)
self.hpp = AdyenHPP(client=self.client)
self.recurring = AdyenRecurring(client=self.client)
Expand All @@ -40,3 +43,4 @@ def __init__(self, **kwargs):
payment = _base_adyen_obj.payment
payout = _base_adyen_obj.payout
checkout = _base_adyen_obj.checkout
binlookup = _base_adyen_obj.binlookup
52 changes: 39 additions & 13 deletions Adyen/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ def __init__(self, username=None, password=None, xapikey=None,
self.http_force = http_force
self.live_endpoint_prefix = live_endpoint_prefix

def _determine_api_url(self, platform, service, action):
@staticmethod
def _determine_api_url(platform, service, action):
"""This returns the Adyen API endpoint based on the provided platform,
service and action.

Expand All @@ -111,7 +112,8 @@ def _determine_api_url(self, platform, service, action):
api_version = settings.API_PAYMENT_VERSION
return '/'.join([base_uri, service, api_version, action])

def _determine_hpp_url(self, platform, action):
@staticmethod
def _determine_hpp_url(platform, action):
"""This returns the Adyen HPP endpoint based on the provided platform,
and action.

Expand Down Expand Up @@ -144,6 +146,9 @@ def _determine_checkout_url(self, platform, action):
by running 'settings.
ENDPOINT_CHECKOUT_LIVE_SUFFIX = 'Your live suffix'"""
raise AdyenEndpointInvalidFormat(errorstring)
else:
raise AdyenEndpointInvalidFormat("invalid config")

if action == "paymentsDetails":
action = "payments/details"
if action == "paymentsResult":
Expand Down Expand Up @@ -272,6 +277,7 @@ def call_api(self, request_data, service, action, idempotency=False,

# platform at self object has highest priority. fallback to root module
# and ensure that it is set to either 'live' or 'test'.
platform = None
if self.platform:
platform = self.platform
elif 'platform' in kwargs:
Expand All @@ -288,13 +294,22 @@ def call_api(self, request_data, service, action, idempotency=False,

if not message.get('merchantAccount'):
message['merchantAccount'] = self.merchant_account

# Add application info
request_data['applicationInfo'] = {
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
if 'applicationInfo' in request_data:
request_data['applicationInfo'].update({
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
}
})
else:
request_data['applicationInfo'] = {
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
}
}
}
# Adyen requires this header to be set and uses the combination of
# merchant account and merchant reference to determine uniqueness.
headers = {}
Expand Down Expand Up @@ -416,6 +431,7 @@ def call_checkout_api(self, request_data, action, **kwargs):

# xapi at self object has highest priority. fallback to root module
# and ensure that it is set.
xapikey = False
if self.xapikey:
xapikey = self.xapikey
elif 'xapikey' in kwargs:
Expand All @@ -428,6 +444,7 @@ def call_checkout_api(self, request_data, action, **kwargs):

# platform at self object has highest priority. fallback to root module
# and ensure that it is set to either 'live' or 'test'.
platform = None
if self.platform:
platform = self.platform
elif 'platform' in kwargs:
Expand All @@ -443,12 +460,20 @@ def call_checkout_api(self, request_data, action, **kwargs):
if not request_data.get('merchantAccount'):
request_data['merchantAccount'] = self.merchant_account

request_data['applicationInfo'] = {
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
if 'applicationInfo' in request_data:
request_data['applicationInfo'].update({
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
}
})
else:
request_data['applicationInfo'] = {
"adyenLibrary": {
"name": settings.LIB_NAME,
"version": settings.LIB_VERSION
}
}
}
# Adyen requires this header to be set and uses the combination of
# merchant account and merchant reference to determine uniqueness.
headers = {}
Expand Down Expand Up @@ -699,7 +724,8 @@ def _handle_http_error(self, url, response_obj, status_code, psp_ref,
psp=psp_ref,
headers=headers, error_code=response_obj.get("errorCode"))

def _error_from_hpp(self, html):
@staticmethod
def _error_from_hpp(html):
# Must be updated when Adyen response is changed:
match_obj = re.search(r'>Error:\s*(.*?)<br', html)
if match_obj:
Expand Down
2 changes: 2 additions & 0 deletions Adyen/httpclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def __init__(self, app_name, user_agent_suffix,
lib_version, force_request=None):
# Check if requests already available, default to urllib
self.user_agent = app_name + " " + user_agent_suffix + lib_version
# In case the app_name is empty
self.user_agent = self.user_agent.strip()
if not force_request:
if requests:
self.request = self._requests_post
Expand Down
61 changes: 42 additions & 19 deletions Adyen/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ def __init__(self, client=None):
super(AdyenRecurring, self).__init__(client=client)
self.service = "Recurring"

def list_recurring_details(self, request="", **kwargs):
def list_recurring_details(self, request, **kwargs):

action = "listRecurringDetails"

return self.client.call_api(request, self.service,
action, **kwargs)

def disable(self, request="", **kwargs):
def disable(self, request, **kwargs):

action = "disable"

Expand All @@ -76,10 +76,10 @@ class AdyenHPP(AdyenServiceBase):
use. If not provided, a new API client will be created.
"""

def __init__(self, client=""):
def __init__(self, client=None):
super(AdyenHPP, self).__init__(client=client)

def directory_lookup(self, request="", **kwargs):
def directory_lookup(self, request, **kwargs):

action = "directory"

Expand All @@ -94,7 +94,7 @@ def directory_lookup(self, request="", **kwargs):

return self.client.call_hpp(request, action)

def hpp_payment(self, request="", skip_details=None, **kwargs):
def hpp_payment(self, request, skip_details=None, **kwargs):

if skip_details:
action = "skipDetails"
Expand Down Expand Up @@ -142,11 +142,11 @@ class AdyenPayment(AdyenServiceBase):
use. If not provided, a new API client will be created.
"""

def __init__(self, client=""):
def __init__(self, client=None):
super(AdyenPayment, self).__init__(client=client)
self.service = "Payment"

def authorise(self, request="", **kwargs):
def authorise(self, request, **kwargs):

action = "authorise"

Expand All @@ -164,19 +164,19 @@ def authorise(self, request="", **kwargs):
return self.client.call_api(request, self.service,
action, **kwargs)

def authorise3d(self, request="", **kwargs):
def authorise3d(self, request, **kwargs):
action = "authorise3d"

return self.client.call_api(request, self.service,
action, **kwargs)

def cancel(self, request="", **kwargs):
def cancel(self, request, **kwargs):
action = "cancel"

return self.client.call_api(request, self.service,
action, **kwargs)

def capture(self, request="", **kwargs):
def capture(self, request, **kwargs):

action = "capture"

Expand All @@ -195,7 +195,7 @@ def capture(self, request="", **kwargs):
action, **kwargs)
return response

def refund(self, request="", **kwargs):
def refund(self, request, **kwargs):

action = "refund"

Expand All @@ -209,7 +209,7 @@ def refund(self, request="", **kwargs):
return self.client.call_api(request, self.service,
action, **kwargs)

def cancel_or_refund(self, request="", **kwargs):
def cancel_or_refund(self, request, **kwargs):
action = "cancelOrRefund"

return self.client.call_api(
Expand Down Expand Up @@ -282,11 +282,11 @@ class AdyenCheckoutApi(AdyenServiceBase):
use. If not provided, a new API client will be created.
"""

def __init__(self, client=""):
def __init__(self, client=None):
super(AdyenCheckoutApi, self).__init__(client=client)
self.service = "Checkout"

def payment_methods(self, request="", **kwargs):
def payment_methods(self, request, **kwargs):
action = "paymentMethods"
if 'merchantAccount' in request:
if request['merchantAccount'] == '':
Expand All @@ -296,22 +296,45 @@ def payment_methods(self, request="", **kwargs):

return self.client.call_checkout_api(request, action, **kwargs)

def payments(self, request="", **kwargs):
def payments(self, request, **kwargs):
action = "payments"
return self.client.call_checkout_api(request, action, **kwargs)

def payments_details(self, request="", **kwargs):
def payments_details(self, request=None, **kwargs):
action = "paymentsDetails"
return self.client.call_checkout_api(request, action, **kwargs)

def payment_session(self, request="", **kwargs):
def payment_session(self, request=None, **kwargs):
action = "paymentSession"
return self.client.call_checkout_api(request, action, **kwargs)

def payment_result(self, request="", **kwargs):
def payment_result(self, request=None, **kwargs):
action = "paymentsResult"
return self.client.call_checkout_api(request, action, **kwargs)

def origin_keys(self, request="", **kwargs):
def origin_keys(self, request=None, **kwargs):
action = "originKeys"
return self.client.call_checkout_api(request, action, **kwargs)


class AdyenBinLookup(AdyenServiceBase):
"""This represents the Adyen API Bin Lookup service.

API call currently implemented: getCostEstimate.
Please refer to the Bin Lookup Manual for specifics around the API.
https://docs.adyen.com/api-explorer/#/BinLookup/v50/overview

Args:
client (AdyenAPIClient, optional): An API client for the service to
use. If not provided, a new API client will be created.
"""

def __init__(self, client=None):
super(AdyenBinLookup, self).__init__(client=client)
self.service = "BinLookup"

def get_cost_estimate(self, request="", **kwargs):

action = "getCostEstimate"

return self.client.call_api(request, self.service, action, **kwargs)
2 changes: 1 addition & 1 deletion Adyen/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
API_RECURRING_VERSION = "v25"
API_PAYMENT_VERSION = "v49"
API_PAYOUT_VERSION = "v30"
LIB_VERSION = "2.2.0"
LIB_VERSION = "2.3.0"
LIB_NAME = "adyen-python-api-library"
50 changes: 50 additions & 0 deletions Adyen/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,53 @@ def is_valid_hmac(dict_object, hmac_key):
merchant_sign = generate_hpp_sig(dict_object, hmac_key)
merchant_sign_str = merchant_sign.decode("utf-8")
return merchant_sign_str == expected_sign


def generate_notification_sig(dict_object, hmac_key):
if 'issuerId' in dict_object:
if dict_object['issuerId'] == "":
del dict_object['issuerId']

if not isinstance(dict_object, dict):
raise ValueError("Must Provide dictionary object")

def escape_val(val):
if isinstance(val, int):
return val
return val.replace('\\', '\\\\').replace(':', '\\:')

hmac_key = binascii.a2b_hex(hmac_key)

request_dict = dict(dict_object)
request_dict['value'] = request_dict['amount']['value']
request_dict['currency'] = request_dict['amount']['currency']

element_orders = [
'pspReference',
'originalReference',
'merchantAccountCode',
'merchantReference',
'value',
'currency',
'eventCode',
'success',
]

signing_string = ':'.join(
map(escape_val, map(str, (
request_dict.get(element, '') for element in element_orders))))

hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256)
return base64.b64encode(hm.digest())


def is_valid_hmac_notification(dict_object, hmac_key):
if 'additionalData' in dict_object:
if dict_object['additionalData']['hmacSignature'] == "":
raise ValueError("Must Provide hmacSignature in additionalData")
else:
expected_sign = dict_object['additionalData']['hmacSignature']
del dict_object['additionalData']
merchant_sign = generate_notification_sig(dict_object, hmac_key)
merchant_sign_str = merchant_sign.decode("utf-8")
return merchant_sign_str == expected_sign
Loading