Skip to content

Commit 1cab2a6

Browse files
committed
Merge pull request #20 from oscaro/feature/error-notifications
Cleanup tests, add support for error and pending payment statuses
2 parents c63d3ec + 76a1b31 commit 1cab2a6

File tree

10 files changed

+519
-564
lines changed

10 files changed

+519
-564
lines changed

adyen/facade.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
logger = logging.getLogger('adyen')
1313

14+
1415
def get_gateway(request, config):
1516
return Gateway({
1617
Constants.IDENTIFIER: config.get_identifier(request),

adyen/gateway.py

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ def _process_response(self, adyen_response, params):
180180

181181

182182
class BaseInteraction:
183+
REQUIRED_FIELDS = ()
184+
OPTIONAL_FIELDS = ()
185+
HASH_KEYS = ()
186+
HASH_FIELD = None
187+
188+
def hash(self):
189+
return self.client._compute_hash(self.HASH_KEYS, self.params)
190+
191+
def validate(self):
192+
self.check_fields()
183193

184194
def check_fields(self):
185195
"""
@@ -204,42 +214,9 @@ def check_fields(self):
204214
)
205215

206216

207-
# ---[ REQUESTS ]---
208-
209-
class BaseRequest(BaseInteraction):
210-
REQUIRED_FIELDS = ()
211-
OPTIONAL_FIELDS = ()
212-
HASH_KEYS = ()
213-
214-
def __init__(self, client, params=None):
215-
216-
if params is None:
217-
params = {}
218-
219-
self.client = client
220-
self.params = params
221-
self.validate()
222-
223-
# Compute request hash.
224-
self.params.update({self.HASH_FIELD: self.hash()})
225-
226-
def validate(self):
227-
self.check_fields()
228-
229-
def hash(self):
230-
return self.client._compute_hash(self.HASH_KEYS, self.params)
231-
232-
233217
# ---[ FORM-BASED REQUESTS ]---
234218

235-
class FormRequest(BaseRequest):
236-
237-
def build_form_fields(self):
238-
return [{'type': 'hidden', 'name': name, 'value': value}
239-
for name, value in self.params.items()]
240-
241-
242-
class PaymentFormRequest(FormRequest):
219+
class PaymentFormRequest(BaseInteraction):
243220
REQUIRED_FIELDS = (
244221
Constants.MERCHANT_ACCOUNT,
245222
Constants.MERCHANT_REFERENCE,
@@ -290,26 +267,28 @@ class PaymentFormRequest(FormRequest):
290267
Constants.OFFSET,
291268
)
292269

270+
def __init__(self, client, params=None):
271+
self.client = client
272+
self.params = params or {}
273+
self.validate()
274+
275+
# Compute request hash.
276+
self.params.update({self.HASH_FIELD: self.hash()})
277+
278+
def build_form_fields(self):
279+
return [{'type': 'hidden', 'name': name, 'value': value}
280+
for name, value in self.params.items()]
281+
293282

294283
# ---[ RESPONSES ]---
295284

296285
class BaseResponse(BaseInteraction):
297-
REQUIRED_FIELDS = ()
298-
OPTIONAL_FIELDS = ()
299-
HASH_FIELD = None
300-
HASH_KEYS = ()
301286

302287
def __init__(self, client, params):
303288
self.client = client
304289
self.secret_key = client.secret_key
305290
self.params = params
306291

307-
def validate(self):
308-
self.check_fields()
309-
310-
def hash(self):
311-
return self.client._compute_hash(self.HASH_KEYS, self.params)
312-
313292
def process(self):
314293
return NotImplemented
315294

@@ -353,7 +332,7 @@ def check_fields(self):
353332
"System communication" setup (instead of the old "notifications" tab
354333
in the settings).
355334
We currently don't need any of that data, so we just drop it
356-
before validating the response.
335+
before validating the notification.
357336
:return:
358337
"""
359338
self.params = {
@@ -404,13 +383,11 @@ def validate(self):
404383
# Check that the transaction has not been tampered with.
405384
received_hash = self.params.get(self.HASH_FIELD)
406385
expected_hash = self.hash()
407-
if expected_hash != received_hash:
386+
if not received_hash or expected_hash != received_hash:
408387
raise InvalidTransactionException(
409-
"The transaction is invalid. "
410-
"This may indicate a fraud attempt."
411-
)
388+
"The transaction is invalid. This may indicate a fraud attempt.")
412389

413390
def process(self):
414-
payment_result = self.params.get(Constants.AUTH_RESULT, None)
391+
payment_result = self.params[Constants.AUTH_RESULT]
415392
accepted = payment_result == Constants.PAYMENT_RESULT_AUTHORISED
416393
return accepted, payment_result, self.params

adyen/scaffold.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ class Scaffold:
1010
# These are the constants that all scaffolds are expected to return
1111
# to a multi-psp application. They might look like those actually returned
1212
# by the psp itself, but that would be a pure coincidence.
13+
# At some point we could discuss merging cancelled & refused & error and just
14+
# ensuring good error messages are returned. I doubt the distinction is
15+
# important to most checkout procedures.
1316
PAYMENT_STATUS_ACCEPTED = 'ACCEPTED'
1417
PAYMENT_STATUS_CANCELLED = 'CANCELLED'
1518
PAYMENT_STATUS_REFUSED = 'REFUSED'
19+
PAYMENT_STATUS_ERROR = 'ERROR'
20+
PAYMENT_STATUS_PENDING = 'PENDING'
1621

1722
# This is the mapping between Adyen-specific and these standard statuses
1823
ADYEN_TO_COMMON_PAYMENT_STATUSES = {
1924
Constants.PAYMENT_RESULT_AUTHORISED: PAYMENT_STATUS_ACCEPTED,
2025
Constants.PAYMENT_RESULT_CANCELLED: PAYMENT_STATUS_CANCELLED,
2126
Constants.PAYMENT_RESULT_REFUSED: PAYMENT_STATUS_REFUSED,
27+
Constants.PAYMENT_RESULT_ERROR: PAYMENT_STATUS_ERROR,
28+
Constants.PAYMENT_RESULT_PENDING: PAYMENT_STATUS_PENDING,
2229
}
2330

2431
def __init__(self):
@@ -76,7 +83,7 @@ def _normalize_feedback(self, feedback):
7683
common to all payment provider backends.
7784
"""
7885
success, adyen_status, details = feedback
79-
common_status = self.ADYEN_TO_COMMON_PAYMENT_STATUSES.get(adyen_status)
86+
common_status = self.ADYEN_TO_COMMON_PAYMENT_STATUSES[adyen_status]
8087
return success, common_status, details
8188

8289
def handle_payment_feedback(self, request):

tests/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from copy import deepcopy
2+
3+
4+
class MockRequest:
5+
6+
def __init__(self, data=None, method='GET', remote_address='127.0.0.1'):
7+
self.method = method
8+
self.META = {}
9+
10+
if method == 'GET':
11+
self.GET = deepcopy(data) or {}
12+
elif method == 'POST':
13+
self.POST = deepcopy(data) or {}
14+
15+
# Most tests use unproxied requests, the case of proxied ones
16+
# is unit-tested by the `test_get_origin_ip_address` method.
17+
if remote_address is not None:
18+
self.META['REMOTE_ADDR'] = remote_address

tests/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@
2121
)
2222

2323
INSTALLED_APPS = ('adyen',)
24+
25+
ADYEN_IDENTIFIER = 'OscaroFR'
26+
ADYEN_SECRET_KEY = 'oscaroscaroscaro'
27+
ADYEN_ACTION_URL = 'https://test.adyen.com/hpp/select.shtml'
28+
ADYEN_SKIN_CODE = 'cqQJKZpg'

0 commit comments

Comments
 (0)