Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow client code to use the Adyen merchantReturnData field. #25

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions adyen/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging

from django.http import HttpResponse
from django.utils.six.moves.urllib import parse

from .gateway import Constants, Gateway, PaymentNotification, PaymentRedirection
from .models import AdyenTransaction
Expand Down Expand Up @@ -76,23 +77,31 @@ def _unpack_details(self, details):
payment_method = details.get(Constants.PAYMENT_METHOD, '')
psp_reference = details.get(Constants.PSP_REFERENCE, '')

# The payment amount is transmitted in a different parameter whether
# we are in the context of a PaymentRedirection (`merchantReturnData`)
# or a PaymentNotification (`value`). Both fields are mandatory in the
# respective context, ensuring we always get back our amount.
# This is, however, not generic in case of using the backend outside
# the oshop project, since it is our decision to store the amount in
# the `merchantReturnData` field. Leaving a TODO here to make this more
# generic at a later date.
amount = int(details.get(Constants.MERCHANT_RETURN_DATA, details.get(Constants.VALUE)))

return {
'amount': amount,
unpacked_details = {
'order_number': order_number,
'payment_method': payment_method,
'psp_reference': psp_reference,
}

# The payment amount is transmitted in a different parameter whether
# we are in the context of a PaymentRedirection (`merchantReturnData`)
# or a PaymentNotification (`value`). Both fields are mandatory in the
# respective context, ensuring we always get back our amount.
if Constants.MERCHANT_RETURN_DATA in details.keys():
return_data = details.get(Constants.MERCHANT_RETURN_DATA)
if '~' in return_data:
# If there was any client-supplied merchant return data, it was
# concatenated to the amount with a tilde.
(amount, user_data) = return_data.split('~', 1)
unpacked_details['amount'] = int(amount)
unpacked_details['merchant_return_data'] = parse.unquote_plus(user_data)
else:
unpacked_details['amount'] = int(return_data)
else:
unpacked_details['amount'] = int(details.get(Constants.VALUE))

return unpacked_details

def _record_audit_trail(self, request, status, txn_details):
"""
Record an AdyenTransaction to keep track of the current payment attempt.
Expand Down
16 changes: 13 additions & 3 deletions adyen/scaffold.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.utils import timezone
from django.utils.six.moves.urllib import parse

from .facade import Facade
from .gateway import Constants, MissingFieldException
Expand Down Expand Up @@ -61,9 +62,6 @@ def get_form_fields(self, request, order_data):
Constants.PAYMENT_AMOUNT: order_data['amount'],
Constants.SHOPPER_LOCALE: order_data['shopper_locale'],
Constants.COUNTRY_CODE: order_data['country_code'],
# Adyen does not provide the payment amount in the return URL, so we store it in
# this field to avoid a database query to get it back then.
Constants.MERCHANT_RETURN_DATA: order_data['amount'],
}

except KeyError:
Expand All @@ -75,6 +73,18 @@ def get_form_fields(self, request, order_data):
return_url = return_url.replace('PAYMENT_PROVIDER_CODE', Constants.ADYEN)
field_specs[Constants.MERCHANT_RETURN_URL] = return_url

# Adyen does not provide the payment amount in the return URL, so we store it in
# the merchantReturnData field to avoid a database query to get it back during
# postback.
# Any user-provided parameters are appended at the end of the amount, separated
# by a tilde.
merchant_return_data = str(order_data['amount'])
return_data = order_data.get('merchant_return_data', None)
if return_data:
merchant_return_data += "~%s" % parse.quote_plus(return_data.encode('utf-8'))

field_specs[Constants.MERCHANT_RETURN_DATA] = merchant_return_data

return Facade().build_payment_form_fields(request, field_specs)

def _normalize_feedback(self, feedback):
Expand Down
55 changes: 45 additions & 10 deletions tests/test_redirects.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
from copy import copy
from django.test import TestCase

from adyen.facade import Facade
from adyen.gateway import MissingFieldException, InvalidTransactionException
from adyen.gateway import MissingFieldException, InvalidTransactionException, Constants
from adyen.models import AdyenTransaction
from adyen.scaffold import Scaffold

Expand Down Expand Up @@ -36,16 +37,8 @@ def test_handle_authorised_payment(self):

assert success
assert status == Scaffold.PAYMENT_STATUS_ACCEPTED
assert details['amount'] == 13894
assert details['ip_address'] == '127.0.0.1'
assert details['method'] == 'adyen'
assert details['psp_reference'] == '8814136447235922'
assert details['status'] == 'AUTHORISED'

# After calling `handle_payment_feedback` there is one authorised
# transaction and no refused transaction in the database.
assert AdyenTransaction.objects.filter(status='AUTHORISED').count() == 1
assert AdyenTransaction.objects.filter(status='REFUSED').count() == 0
self._assert_accepted_details(details)

# We delete the previously recorded AdyenTransaction.
AdyenTransaction.objects.filter(status='AUTHORISED').delete()
Expand Down Expand Up @@ -75,6 +68,18 @@ def test_handle_authorised_payment(self):
assert AdyenTransaction.objects.filter(status='AUTHORISED').count() == 1
assert AdyenTransaction.objects.filter(status='REFUSED').count() == 0

def _assert_accepted_details(self, details):
assert details['amount'] == 13894
assert details['ip_address'] == '127.0.0.1'
assert details['method'] == 'adyen'
assert details['psp_reference'] == '8814136447235922'
assert details['status'] == 'AUTHORISED'

# After calling `handle_payment_feedback` there is one authorised
# transaction and no refused transaction in the database.
assert AdyenTransaction.objects.filter(status='AUTHORISED').count() == 1
assert AdyenTransaction.objects.filter(status='REFUSED').count() == 0

def test_handle_authorized_payment_if_no_ip_address_was_found(self):
"""
A slight variation on the previous test.
Expand Down Expand Up @@ -196,3 +201,33 @@ def test_handle_pending_payment(self):
assert (not success) and (status == Scaffold.PAYMENT_STATUS_PENDING)

assert AdyenTransaction.objects.filter(status='PENDING').count() == 1

def test_handle_merchant_return_data_with_magic_char(self):
data = AUTHORISED_PAYMENT_PARAMS_GET.copy()
custom_return_data = 'custom~return~data'
data[Constants.MERCHANT_RETURN_DATA] += '~%s' % custom_return_data
data[Constants.MERCHANT_SIG] = '86AroR/R2wcDv2NcrdKdB8TLx4s='
request = MockRequest(data)
success, status, details = Scaffold().handle_payment_feedback(request)

assert success
assert status == Scaffold.PAYMENT_STATUS_ACCEPTED

self._assert_accepted_details(details)

assert details['merchant_return_data'] == custom_return_data

def test_handle_encoded_merchant_return_data(self):
data = AUTHORISED_PAYMENT_PARAMS_GET.copy()
custom_return_data = '%F0%9F%8E%B8+G%C3%BCit%C3%A4r'
data[Constants.MERCHANT_RETURN_DATA] += '~%s' % custom_return_data
data[Constants.MERCHANT_SIG] = 'qOF1wyTkRawrwrqPW0PRmQm7fc0='
request = MockRequest(data)
success, status, details = Scaffold().handle_payment_feedback(request)

assert success
assert status == Scaffold.PAYMENT_STATUS_ACCEPTED

self._assert_accepted_details(details)

assert details['merchant_return_data'] == u'\U0001f3b8 G\xfcit\xe4r'
2 changes: 1 addition & 1 deletion tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{'type': 'hidden', 'name': 'currencyCode', 'value': 'EUR'},
{'type': 'hidden', 'name': 'merchantAccount', 'value': settings.ADYEN_IDENTIFIER},
{'type': 'hidden', 'name': 'merchantReference', 'value': '00000000123'},
{'type': 'hidden', 'name': 'merchantReturnData', 'value': 123},
{'type': 'hidden', 'name': 'merchantReturnData', 'value': '123'},
{'type': 'hidden', 'name': 'merchantSig', 'value': 'kKvzRvx7wiPLrl8t8+owcmMuJZM='},
{'type': 'hidden', 'name': 'paymentAmount', 'value': 123},
{'type': 'hidden', 'name': 'resURL', 'value': TEST_RETURN_URL},
Expand Down