Skip to content

Commit 04ffb98

Browse files
authored
Merge pull request #107 from Adyen/develop
2 parents d89344c + 167314d commit 04ffb98

21 files changed

+576
-212
lines changed

Adyen/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
from .client import AdyenClient
1616
from .services import (
1717
AdyenBase,
18+
AdyenBinLookup,
1819
AdyenRecurring,
1920
AdyenPayment,
2021
AdyenThirdPartyPayout,
2122
AdyenHPP,
22-
AdyenCheckoutApi)
23+
AdyenCheckoutApi
24+
)
2325

2426
from .httpclient import HTTPClient
2527

@@ -28,6 +30,7 @@ class Adyen(AdyenBase):
2830
def __init__(self, **kwargs):
2931
self.client = AdyenClient(**kwargs)
3032
self.payment = AdyenPayment(client=self.client)
33+
self.binlookup = AdyenBinLookup(client=self.client)
3134
self.payout = AdyenThirdPartyPayout(client=self.client)
3235
self.hpp = AdyenHPP(client=self.client)
3336
self.recurring = AdyenRecurring(client=self.client)
@@ -40,3 +43,4 @@ def __init__(self, **kwargs):
4043
payment = _base_adyen_obj.payment
4144
payout = _base_adyen_obj.payout
4245
checkout = _base_adyen_obj.checkout
46+
binlookup = _base_adyen_obj.binlookup

Adyen/client.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def __init__(self, username=None, password=None, xapikey=None,
9393
self.http_force = http_force
9494
self.live_endpoint_prefix = live_endpoint_prefix
9595

96-
def _determine_api_url(self, platform, service, action):
96+
@staticmethod
97+
def _determine_api_url(platform, service, action):
9798
"""This returns the Adyen API endpoint based on the provided platform,
9899
service and action.
99100
@@ -111,7 +112,8 @@ def _determine_api_url(self, platform, service, action):
111112
api_version = settings.API_PAYMENT_VERSION
112113
return '/'.join([base_uri, service, api_version, action])
113114

114-
def _determine_hpp_url(self, platform, action):
115+
@staticmethod
116+
def _determine_hpp_url(platform, action):
115117
"""This returns the Adyen HPP endpoint based on the provided platform,
116118
and action.
117119
@@ -144,6 +146,9 @@ def _determine_checkout_url(self, platform, action):
144146
by running 'settings.
145147
ENDPOINT_CHECKOUT_LIVE_SUFFIX = 'Your live suffix'"""
146148
raise AdyenEndpointInvalidFormat(errorstring)
149+
else:
150+
raise AdyenEndpointInvalidFormat("invalid config")
151+
147152
if action == "paymentsDetails":
148153
action = "payments/details"
149154
if action == "paymentsResult":
@@ -272,6 +277,7 @@ def call_api(self, request_data, service, action, idempotency=False,
272277

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

289295
if not message.get('merchantAccount'):
290296
message['merchantAccount'] = self.merchant_account
297+
291298
# Add application info
292-
request_data['applicationInfo'] = {
293-
"adyenLibrary": {
294-
"name": settings.LIB_NAME,
295-
"version": settings.LIB_VERSION
299+
if 'applicationInfo' in request_data:
300+
request_data['applicationInfo'].update({
301+
"adyenLibrary": {
302+
"name": settings.LIB_NAME,
303+
"version": settings.LIB_VERSION
304+
}
305+
})
306+
else:
307+
request_data['applicationInfo'] = {
308+
"adyenLibrary": {
309+
"name": settings.LIB_NAME,
310+
"version": settings.LIB_VERSION
311+
}
296312
}
297-
}
298313
# Adyen requires this header to be set and uses the combination of
299314
# merchant account and merchant reference to determine uniqueness.
300315
headers = {}
@@ -416,6 +431,7 @@ def call_checkout_api(self, request_data, action, **kwargs):
416431

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

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

446-
request_data['applicationInfo'] = {
447-
"adyenLibrary": {
448-
"name": settings.LIB_NAME,
449-
"version": settings.LIB_VERSION
463+
if 'applicationInfo' in request_data:
464+
request_data['applicationInfo'].update({
465+
"adyenLibrary": {
466+
"name": settings.LIB_NAME,
467+
"version": settings.LIB_VERSION
468+
}
469+
})
470+
else:
471+
request_data['applicationInfo'] = {
472+
"adyenLibrary": {
473+
"name": settings.LIB_NAME,
474+
"version": settings.LIB_VERSION
475+
}
450476
}
451-
}
452477
# Adyen requires this header to be set and uses the combination of
453478
# merchant account and merchant reference to determine uniqueness.
454479
headers = {}
@@ -699,7 +724,8 @@ def _handle_http_error(self, url, response_obj, status_code, psp_ref,
699724
psp=psp_ref,
700725
headers=headers, error_code=response_obj.get("errorCode"))
701726

702-
def _error_from_hpp(self, html):
727+
@staticmethod
728+
def _error_from_hpp(html):
703729
# Must be updated when Adyen response is changed:
704730
match_obj = re.search(r'>Error:\s*(.*?)<br', html)
705731
if match_obj:

Adyen/httpclient.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def __init__(self, app_name, user_agent_suffix,
3939
lib_version, force_request=None):
4040
# Check if requests already available, default to urllib
4141
self.user_agent = app_name + " " + user_agent_suffix + lib_version
42+
# In case the app_name is empty
43+
self.user_agent = self.user_agent.strip()
4244
if not force_request:
4345
if requests:
4446
self.request = self._requests_post

Adyen/services.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ def __init__(self, client=None):
4444
super(AdyenRecurring, self).__init__(client=client)
4545
self.service = "Recurring"
4646

47-
def list_recurring_details(self, request="", **kwargs):
47+
def list_recurring_details(self, request, **kwargs):
4848

4949
action = "listRecurringDetails"
5050

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

54-
def disable(self, request="", **kwargs):
54+
def disable(self, request, **kwargs):
5555

5656
action = "disable"
5757

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

79-
def __init__(self, client=""):
79+
def __init__(self, client=None):
8080
super(AdyenHPP, self).__init__(client=client)
8181

82-
def directory_lookup(self, request="", **kwargs):
82+
def directory_lookup(self, request, **kwargs):
8383

8484
action = "directory"
8585

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

9595
return self.client.call_hpp(request, action)
9696

97-
def hpp_payment(self, request="", skip_details=None, **kwargs):
97+
def hpp_payment(self, request, skip_details=None, **kwargs):
9898

9999
if skip_details:
100100
action = "skipDetails"
@@ -142,11 +142,11 @@ class AdyenPayment(AdyenServiceBase):
142142
use. If not provided, a new API client will be created.
143143
"""
144144

145-
def __init__(self, client=""):
145+
def __init__(self, client=None):
146146
super(AdyenPayment, self).__init__(client=client)
147147
self.service = "Payment"
148148

149-
def authorise(self, request="", **kwargs):
149+
def authorise(self, request, **kwargs):
150150

151151
action = "authorise"
152152

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

167-
def authorise3d(self, request="", **kwargs):
167+
def authorise3d(self, request, **kwargs):
168168
action = "authorise3d"
169169

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

173-
def cancel(self, request="", **kwargs):
173+
def cancel(self, request, **kwargs):
174174
action = "cancel"
175175

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

179-
def capture(self, request="", **kwargs):
179+
def capture(self, request, **kwargs):
180180

181181
action = "capture"
182182

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

198-
def refund(self, request="", **kwargs):
198+
def refund(self, request, **kwargs):
199199

200200
action = "refund"
201201

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

212-
def cancel_or_refund(self, request="", **kwargs):
212+
def cancel_or_refund(self, request, **kwargs):
213213
action = "cancelOrRefund"
214214

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

285-
def __init__(self, client=""):
285+
def __init__(self, client=None):
286286
super(AdyenCheckoutApi, self).__init__(client=client)
287287
self.service = "Checkout"
288288

289-
def payment_methods(self, request="", **kwargs):
289+
def payment_methods(self, request, **kwargs):
290290
action = "paymentMethods"
291291
if 'merchantAccount' in request:
292292
if request['merchantAccount'] == '':
@@ -296,22 +296,45 @@ def payment_methods(self, request="", **kwargs):
296296

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

299-
def payments(self, request="", **kwargs):
299+
def payments(self, request, **kwargs):
300300
action = "payments"
301301
return self.client.call_checkout_api(request, action, **kwargs)
302302

303-
def payments_details(self, request="", **kwargs):
303+
def payments_details(self, request=None, **kwargs):
304304
action = "paymentsDetails"
305305
return self.client.call_checkout_api(request, action, **kwargs)
306306

307-
def payment_session(self, request="", **kwargs):
307+
def payment_session(self, request=None, **kwargs):
308308
action = "paymentSession"
309309
return self.client.call_checkout_api(request, action, **kwargs)
310310

311-
def payment_result(self, request="", **kwargs):
311+
def payment_result(self, request=None, **kwargs):
312312
action = "paymentsResult"
313313
return self.client.call_checkout_api(request, action, **kwargs)
314314

315-
def origin_keys(self, request="", **kwargs):
315+
def origin_keys(self, request=None, **kwargs):
316316
action = "originKeys"
317317
return self.client.call_checkout_api(request, action, **kwargs)
318+
319+
320+
class AdyenBinLookup(AdyenServiceBase):
321+
"""This represents the Adyen API Bin Lookup service.
322+
323+
API call currently implemented: getCostEstimate.
324+
Please refer to the Bin Lookup Manual for specifics around the API.
325+
https://docs.adyen.com/api-explorer/#/BinLookup/v50/overview
326+
327+
Args:
328+
client (AdyenAPIClient, optional): An API client for the service to
329+
use. If not provided, a new API client will be created.
330+
"""
331+
332+
def __init__(self, client=None):
333+
super(AdyenBinLookup, self).__init__(client=client)
334+
self.service = "BinLookup"
335+
336+
def get_cost_estimate(self, request="", **kwargs):
337+
338+
action = "getCostEstimate"
339+
340+
return self.client.call_api(request, self.service, action, **kwargs)

Adyen/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
API_RECURRING_VERSION = "v25"
1010
API_PAYMENT_VERSION = "v49"
1111
API_PAYOUT_VERSION = "v30"
12-
LIB_VERSION = "2.2.0"
12+
LIB_VERSION = "2.3.0"
1313
LIB_NAME = "adyen-python-api-library"

Adyen/util.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,53 @@ def is_valid_hmac(dict_object, hmac_key):
4444
merchant_sign = generate_hpp_sig(dict_object, hmac_key)
4545
merchant_sign_str = merchant_sign.decode("utf-8")
4646
return merchant_sign_str == expected_sign
47+
48+
49+
def generate_notification_sig(dict_object, hmac_key):
50+
if 'issuerId' in dict_object:
51+
if dict_object['issuerId'] == "":
52+
del dict_object['issuerId']
53+
54+
if not isinstance(dict_object, dict):
55+
raise ValueError("Must Provide dictionary object")
56+
57+
def escape_val(val):
58+
if isinstance(val, int):
59+
return val
60+
return val.replace('\\', '\\\\').replace(':', '\\:')
61+
62+
hmac_key = binascii.a2b_hex(hmac_key)
63+
64+
request_dict = dict(dict_object)
65+
request_dict['value'] = request_dict['amount']['value']
66+
request_dict['currency'] = request_dict['amount']['currency']
67+
68+
element_orders = [
69+
'pspReference',
70+
'originalReference',
71+
'merchantAccountCode',
72+
'merchantReference',
73+
'value',
74+
'currency',
75+
'eventCode',
76+
'success',
77+
]
78+
79+
signing_string = ':'.join(
80+
map(escape_val, map(str, (
81+
request_dict.get(element, '') for element in element_orders))))
82+
83+
hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256)
84+
return base64.b64encode(hm.digest())
85+
86+
87+
def is_valid_hmac_notification(dict_object, hmac_key):
88+
if 'additionalData' in dict_object:
89+
if dict_object['additionalData']['hmacSignature'] == "":
90+
raise ValueError("Must Provide hmacSignature in additionalData")
91+
else:
92+
expected_sign = dict_object['additionalData']['hmacSignature']
93+
del dict_object['additionalData']
94+
merchant_sign = generate_notification_sig(dict_object, hmac_key)
95+
merchant_sign_str = merchant_sign.decode("utf-8")
96+
return merchant_sign_str == expected_sign

0 commit comments

Comments
 (0)