Skip to content

Commit

Permalink
Merge pull request #20 from oscaro/feature/error-notifications
Browse files Browse the repository at this point in the history
Cleanup tests, add support for error and pending payment statuses
  • Loading branch information
maiksprenger committed Oct 7, 2015
2 parents c63d3ec + 76a1b31 commit 1cab2a6
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 564 deletions.
1 change: 1 addition & 0 deletions adyen/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

logger = logging.getLogger('adyen')


def get_gateway(request, config):
return Gateway({
Constants.IDENTIFIER: config.get_identifier(request),
Expand Down
77 changes: 27 additions & 50 deletions adyen/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ def _process_response(self, adyen_response, params):


class BaseInteraction:
REQUIRED_FIELDS = ()
OPTIONAL_FIELDS = ()
HASH_KEYS = ()
HASH_FIELD = None

def hash(self):
return self.client._compute_hash(self.HASH_KEYS, self.params)

def validate(self):
self.check_fields()

def check_fields(self):
"""
Expand All @@ -204,42 +214,9 @@ def check_fields(self):
)


# ---[ REQUESTS ]---

class BaseRequest(BaseInteraction):
REQUIRED_FIELDS = ()
OPTIONAL_FIELDS = ()
HASH_KEYS = ()

def __init__(self, client, params=None):

if params is None:
params = {}

self.client = client
self.params = params
self.validate()

# Compute request hash.
self.params.update({self.HASH_FIELD: self.hash()})

def validate(self):
self.check_fields()

def hash(self):
return self.client._compute_hash(self.HASH_KEYS, self.params)


# ---[ FORM-BASED REQUESTS ]---

class FormRequest(BaseRequest):

def build_form_fields(self):
return [{'type': 'hidden', 'name': name, 'value': value}
for name, value in self.params.items()]


class PaymentFormRequest(FormRequest):
class PaymentFormRequest(BaseInteraction):
REQUIRED_FIELDS = (
Constants.MERCHANT_ACCOUNT,
Constants.MERCHANT_REFERENCE,
Expand Down Expand Up @@ -290,26 +267,28 @@ class PaymentFormRequest(FormRequest):
Constants.OFFSET,
)

def __init__(self, client, params=None):
self.client = client
self.params = params or {}
self.validate()

# Compute request hash.
self.params.update({self.HASH_FIELD: self.hash()})

def build_form_fields(self):
return [{'type': 'hidden', 'name': name, 'value': value}
for name, value in self.params.items()]


# ---[ RESPONSES ]---

class BaseResponse(BaseInteraction):
REQUIRED_FIELDS = ()
OPTIONAL_FIELDS = ()
HASH_FIELD = None
HASH_KEYS = ()

def __init__(self, client, params):
self.client = client
self.secret_key = client.secret_key
self.params = params

def validate(self):
self.check_fields()

def hash(self):
return self.client._compute_hash(self.HASH_KEYS, self.params)

def process(self):
return NotImplemented

Expand Down Expand Up @@ -353,7 +332,7 @@ def check_fields(self):
"System communication" setup (instead of the old "notifications" tab
in the settings).
We currently don't need any of that data, so we just drop it
before validating the response.
before validating the notification.
:return:
"""
self.params = {
Expand Down Expand Up @@ -404,13 +383,11 @@ def validate(self):
# Check that the transaction has not been tampered with.
received_hash = self.params.get(self.HASH_FIELD)
expected_hash = self.hash()
if expected_hash != received_hash:
if not received_hash or expected_hash != received_hash:
raise InvalidTransactionException(
"The transaction is invalid. "
"This may indicate a fraud attempt."
)
"The transaction is invalid. This may indicate a fraud attempt.")

def process(self):
payment_result = self.params.get(Constants.AUTH_RESULT, None)
payment_result = self.params[Constants.AUTH_RESULT]
accepted = payment_result == Constants.PAYMENT_RESULT_AUTHORISED
return accepted, payment_result, self.params
9 changes: 8 additions & 1 deletion adyen/scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@ class Scaffold:
# These are the constants that all scaffolds are expected to return
# to a multi-psp application. They might look like those actually returned
# by the psp itself, but that would be a pure coincidence.
# At some point we could discuss merging cancelled & refused & error and just
# ensuring good error messages are returned. I doubt the distinction is
# important to most checkout procedures.
PAYMENT_STATUS_ACCEPTED = 'ACCEPTED'
PAYMENT_STATUS_CANCELLED = 'CANCELLED'
PAYMENT_STATUS_REFUSED = 'REFUSED'
PAYMENT_STATUS_ERROR = 'ERROR'
PAYMENT_STATUS_PENDING = 'PENDING'

# This is the mapping between Adyen-specific and these standard statuses
ADYEN_TO_COMMON_PAYMENT_STATUSES = {
Constants.PAYMENT_RESULT_AUTHORISED: PAYMENT_STATUS_ACCEPTED,
Constants.PAYMENT_RESULT_CANCELLED: PAYMENT_STATUS_CANCELLED,
Constants.PAYMENT_RESULT_REFUSED: PAYMENT_STATUS_REFUSED,
Constants.PAYMENT_RESULT_ERROR: PAYMENT_STATUS_ERROR,
Constants.PAYMENT_RESULT_PENDING: PAYMENT_STATUS_PENDING,
}

def __init__(self):
Expand Down Expand Up @@ -76,7 +83,7 @@ def _normalize_feedback(self, feedback):
common to all payment provider backends.
"""
success, adyen_status, details = feedback
common_status = self.ADYEN_TO_COMMON_PAYMENT_STATUSES.get(adyen_status)
common_status = self.ADYEN_TO_COMMON_PAYMENT_STATUSES[adyen_status]
return success, common_status, details

def handle_payment_feedback(self, request):
Expand Down
18 changes: 18 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from copy import deepcopy


class MockRequest:

def __init__(self, data=None, method='GET', remote_address='127.0.0.1'):
self.method = method
self.META = {}

if method == 'GET':
self.GET = deepcopy(data) or {}
elif method == 'POST':
self.POST = deepcopy(data) or {}

# Most tests use unproxied requests, the case of proxied ones
# is unit-tested by the `test_get_origin_ip_address` method.
if remote_address is not None:
self.META['REMOTE_ADDR'] = remote_address
5 changes: 5 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@
)

INSTALLED_APPS = ('adyen',)

ADYEN_IDENTIFIER = 'OscaroFR'
ADYEN_SECRET_KEY = 'oscaroscaroscaro'
ADYEN_ACTION_URL = 'https://test.adyen.com/hpp/select.shtml'
ADYEN_SKIN_CODE = 'cqQJKZpg'
Loading

0 comments on commit 1cab2a6

Please sign in to comment.