diff --git a/adyen/facade.py b/adyen/facade.py index dd36b77..c5581d6 100644 --- a/adyen/facade.py +++ b/adyen/facade.py @@ -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 @@ -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. diff --git a/adyen/scaffold.py b/adyen/scaffold.py index 5414d8d..43549e6 100644 --- a/adyen/scaffold.py +++ b/adyen/scaffold.py @@ -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 @@ -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: @@ -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): diff --git a/tests/test_redirects.py b/tests/test_redirects.py index cfc63c3..ec83a28 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -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 @@ -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() @@ -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. @@ -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' diff --git a/tests/test_requests.py b/tests/test_requests.py index ef4b1c2..623ef98 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -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},