From a2edc40a47ec4cb45426599692f5f7e7c222d710 Mon Sep 17 00:00:00 2001 From: Marcello Stanisci Date: Fri, 3 Nov 2017 11:00:39 +0100 Subject: Adding the Taler payment system. --- doc/modules.rst | 29 ++++- payments/locale/en/LC_MESSAGES/django.po | 24 +++- payments/locale/it/LC_MESSAGES/django.po | 24 +++- payments/locale/ru/LC_MESSAGES/django.po | 20 ++- payments/taler/__init__.py | 156 ++++++++++++++++++++++++ payments/taler/amount.py | 120 ++++++++++++++++++ payments/taler/test_amount.py | 63 ++++++++++ payments/taler/test_taler.py | 155 +++++++++++++++++++++++ payments/templates/payments/taler_fallback.html | 8 ++ 9 files changed, 589 insertions(+), 10 deletions(-) create mode 100644 payments/taler/__init__.py create mode 100644 payments/taler/amount.py create mode 100644 payments/taler/test_amount.py create mode 100644 payments/taler/test_taler.py create mode 100644 payments/templates/payments/taler_fallback.html diff --git a/doc/modules.rst b/doc/modules.rst index 745a7bc..a547b7a 100644 --- a/doc/modules.rst +++ b/doc/modules.rst @@ -255,7 +255,7 @@ Example:: This backend does not support fraud detection. Sofort.com --------- +---------- .. class:: payments.sofort.SofortProvider(key, id, project_id[, endpoint='https://api.sofort.com/api/xml']) @@ -301,3 +301,30 @@ Example:: This backend does not support fraud detection. + +Taler +----- + +.. class:: payments.taler.TalerProvider(self, backend_url, address, name, jurisdiction, instance=None) + + This backend implements payments using `Taler `_. + + :param backend_url: Backend's base URL. + :param instance: Merchant instance. + :param address: Merchant physical address. + :param name: Merchant/shop name. + :param jurisdiction: Jurisdiction where the merchant operates. + +Example:: + + PAYMENT_VARIANTS = { + 'taler': ('payments.taler.TalerProvider', + {'backend_url': 'http://backend.test.taler.net/', + 'address': 'US', + 'jurisdiction': 'US', + 'name': 'Taler tester shop'}) + } + +.. This backend does not report fraud to Saleor, as fraud in + the classical sense is not possible with Taler. Naturally, + protocol failures are detected and reported (to the wallet). diff --git a/payments/locale/en/LC_MESSAGES/django.po b/payments/locale/en/LC_MESSAGES/django.po index d063dda..de8542a 100644 --- a/payments/locale/en/LC_MESSAGES/django.po +++ b/payments/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-13 13:13+0100\n" +"POT-Creation-Date: 2017-10-20 14:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -158,11 +158,11 @@ msgstr "" msgid "We were unable to complete the transaction. Please try again later." msgstr "" -#: payments/cybersource/forms.py:51 +#: payments/cybersource/forms.py:52 msgid "fingerprint" msgstr "" -#: payments/cybersource/forms.py:68 payments/stripe/forms.py:52 +#: payments/cybersource/forms.py:69 payments/stripe/forms.py:52 msgid "This payment has already been processed." msgstr "" @@ -205,6 +205,24 @@ msgstr "" msgid "Total payment" msgstr "" +#: payments/taler/__init__.py:89 +msgid "" +"Taler wallet disabled, please go back and either enable the wallet or pick " +"another payment method. Please keep the wallet enabled if you pick Taler " +"again!" +msgstr "" + +#: payments/taler/__init__.py:117 +msgid "" +"Your wallet seems to have been disabled while processing this payment; your " +"coins didn't get spent though. Please go back in the checkout page and " +"restart the payment with the method you desire." +msgstr "" + +#: payments/templates/payments/taler_fallback.html:5 +msgid "Payment unsuccessful" +msgstr "" + #: payments/utils.py:8 msgid "Month" msgstr "" diff --git a/payments/locale/it/LC_MESSAGES/django.po b/payments/locale/it/LC_MESSAGES/django.po index bfc3f8c..cb5ca7a 100644 --- a/payments/locale/it/LC_MESSAGES/django.po +++ b/payments/locale/it/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-13 13:13+0100\n" +"POT-Creation-Date: 2017-10-20 14:17+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Marco Badan , 2017\n" "Language-Team: Italian (https://www.transifex.com/mirumee/teams/34782/it/)\n" @@ -177,11 +177,11 @@ msgstr "" msgid "We were unable to complete the transaction. Please try again later." msgstr "Non siamo riusciti a completare la transazione. Riprova più tardi." -#: payments/cybersource/forms.py:51 +#: payments/cybersource/forms.py:52 msgid "fingerprint" msgstr "fingerprint" -#: payments/cybersource/forms.py:68 payments/stripe/forms.py:52 +#: payments/cybersource/forms.py:69 payments/stripe/forms.py:52 msgid "This payment has already been processed." msgstr "Questo pagamento è già stato processato." @@ -226,6 +226,24 @@ msgstr "controllo frode" msgid "Total payment" msgstr "Totale pagamento" +#: payments/taler/__init__.py:89 +msgid "" +"Taler wallet disabled, please go back and either enable the wallet or pick " +"another payment method. Please keep the wallet enabled if you pick Taler " +"again!" +msgstr "" + +#: payments/taler/__init__.py:117 +msgid "" +"Your wallet seems to have been disabled while processing this payment; your " +"coins didn't get spent though. Please go back in the checkout page and " +"restart the payment with the method you desire." +msgstr "" + +#: payments/templates/payments/taler_fallback.html:5 +msgid "Payment unsuccessful" +msgstr "" + #: payments/utils.py:8 msgid "Month" msgstr "Mese" diff --git a/payments/locale/ru/LC_MESSAGES/django.po b/payments/locale/ru/LC_MESSAGES/django.po index 31d44bc..d9b2e3e 100644 --- a/payments/locale/ru/LC_MESSAGES/django.po +++ b/payments/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-13 13:12+0100\n" +"POT-Creation-Date: 2017-10-12 06:57-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Weronika Terpilowska , 2017\n" "Language-Team: Russian (https://www.transifex.com/mirumee/teams/34782/ru/)\n" @@ -174,11 +174,11 @@ msgstr "" msgid "We were unable to complete the transaction. Please try again later." msgstr "Мы не смогли провести транзакцию. Пожалуйста, попробуйте позже." -#: payments/cybersource/forms.py:51 +#: payments/cybersource/forms.py:52 msgid "fingerprint" msgstr "отпечаток пальца" -#: payments/cybersource/forms.py:68 payments/stripe/forms.py:52 +#: payments/cybersource/forms.py:69 payments/stripe/forms.py:52 msgid "This payment has already been processed." msgstr "Этот платеж уже был обработан." @@ -223,6 +223,20 @@ msgstr "проверка транзакции" msgid "Total payment" msgstr "Общая сумма" +#: payments/taler/__init__.py:92 +msgid "" +"Taler wallet disabled, please go back and either enable the wallet or pick " +"another payment method. Please keep the wallet enabled if you pick Taler " +"again!" +msgstr "" + +#: payments/taler/__init__.py:120 +msgid "" +"Your wallet seems to have been disabled while processing this payment; your " +"coins didn't get spent though. Please go back in the checkout page and " +"restart the payment with the method you desire." +msgstr "" + #: payments/utils.py:8 msgid "Month" msgstr "Месяц" diff --git a/payments/taler/__init__.py b/payments/taler/__init__.py new file mode 100644 index 0000000..191150c --- /dev/null +++ b/payments/taler/__init__.py @@ -0,0 +1,156 @@ +# This file is part of DJANGO-PAYMENTS +# (C) 2017 Taler Systems SA +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci + +from django.utils.translation import ugettext as _ +from django.template.loader import render_to_string +import json +from django.shortcuts import redirect +from ..core import BasicProvider, get_base_url +from .. import RedirectNeeded, PaymentStatus +import re +from django.http import HttpResponse, JsonResponse +import requests +from urllib.parse import urljoin +import logging +from django.conf import settings +from .amount import Amount, BadAmount + +logger = logging.getLogger(__name__) + +class TalerProvider(BasicProvider): + ''' + GNU Taler payment provider + ''' + + def __init__(self, backend_url, address, name, jurisdiction, instance=None): + + # The backend URL is served by the C backend, it is + # used to sign data coming from the frontend and to + # communicate with the exchange. + # Its URL must end with a slash, like 'http://backend.demo.taler.net/'. + self.backend_url = backend_url + # Token which identifies this frontend to the backend. In fact, + # any backend can support multiple frontends, and 'None' is the + # default one. + self.instance = instance + # Physical address and jurisdiction, and shop name + self.address = address + self.name = name + self.jurisdiction = jurisdiction + super(TalerProvider, self) + + # This function gets called when the user chooses the + # payment method to use. It will redirect the user to + # the page returning 402+contract_url. + def get_form(self, payment, data=None): + raise RedirectNeeded(self.get_return_url(payment)) + + # Design note: in django-payments, there is usually ONE URL that + # processes the payment, thus decisions are taken on the basis of + # the _state_ of the payment, rather than a particular endpoint. + # For example, it is perfectly normal having fulfillment and pay + # URL being the same thing. + + def process_data(self, payment, request): + + # Very first branch taken. It returns the 402 status + # plus the contract generation URL. + if payment.status == PaymentStatus.WAITING: + wallet_not_found_msg = _('Taler wallet disabled. Please' \ + ' either enable the wallet or pick another payment method.') + data = render_to_string('payments/taler_fallback.html', + {'msg': wallet_not_found_msg}) + response = HttpResponse(data, status=402) + + response['X-Taler-Contract-Url'] = self.get_return_url(payment) + payment.change_status(PaymentStatus.INPUT) + return response + + # Listen for contract generation. + if payment.status == PaymentStatus.INPUT: + try: + total_amount = Amount.parse('%s:%0.2f' % (payment.currency, payment.total)).dump() + order = { + 'summary': payment.message, + 'nonce': request.GET.get('nonce'), + 'amount': total_amount, + 'products': [{'description': payment.description, + 'quantity': 1, + 'product_id': 0, + 'price': total_amount}], + 'fulfillment_url': self.get_return_url(payment), + 'pay_url': self.get_return_url(payment), + 'merchant': { + 'instance': self.instance, + 'address': self.address, + 'name': self.name, + 'jurisdiction': self.jurisdiction}, + 'extra': {}} + except BadAmount as e: + logger.error('Malformed amount: %s' % e.faulty_str) + data = {'error': _('Internal error generating contract'), + 'detail': _('Could not parse amount')} + return JsonResponse(data, status=500) + + try: + r = requests.post(urljoin(self.backend_url, 'proposal'), + json={'order': order}) + except requests.RequestException as e: + logger.error(e) + return JsonResponse({'error': 'Internal server error', + 'detail': 'Could not reach the backend'}, status=500) + if r.status_code == 200: + payment.change_status(PaymentStatus.PREAUTH) + return JsonResponse(r.json(), status=r.status_code) + + # This is responsible to both execute the payment and receive it. + # When the wallet attempts to GET it, it returns the 402 which + # executes the payment, whereas POSTing to it triggers the /pay behaviour. + if payment.status == PaymentStatus.PREAUTH: + if request.method == 'POST': + try: + r = requests.post(urljoin(self.backend_url, 'pay'), + json=json.loads(request.body.decode('utf-8'))) + except requests.RequestException as e: + logger.error(e) + return JsonResponse({'error': 'Internal server error', + 'detail': 'Could not reach the backend'}, status=500) + if r.status_code == 200: + payment.change_status(PaymentStatus.CONFIRMED) + return JsonResponse(r.json(), status=r.status_code) + + if request.method == 'GET': + wallet_not_found_msg = _('Taler wallet disabled. Please' \ + ' either enable the wallet or pick another payment method.') + data = render_to_string('payments/taler_fallback.html', + {'msg': wallet_not_found_msg}) + response = HttpResponse(data, status=402) + response['X-Taler-Contract-Url'] = self.get_return_url(payment) + response['X-Taler-Offer-Url'] = get_base_url() + return response + + # Taken when the payment has gone through, redirect to persistent + # fulfillment page. + if payment.status == PaymentStatus.CONFIRMED: + # returns absolute url + return redirect(payment.get_success_url()) + + # This should _never_ happen + return JsonResponse({'error': 'Internal server error', + 'detail': 'Unknown payment status!'}, status=500) diff --git a/payments/taler/amount.py b/payments/taler/amount.py new file mode 100644 index 0000000..5bd2354 --- /dev/null +++ b/payments/taler/amount.py @@ -0,0 +1,120 @@ +# This file is part of TALER +# (C) 2017 Taler Systems SA +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci +# @version 0.0 +# @repository https://git.taler.net/copylib.git/ +# This code is "copylib", it is versioned under the Git repository +# mentioned above, and it is meant to be manually copied into any project +# which might need it. + +class BadAmount(Exception): + def __init__(self, faulty_str): + self.faulty_str = faulty_str + +class Amount: + # How many "fraction" units make one "value" unit of currency + # (Taler requires 10^8). Do not change this 'constant'. + @staticmethod + def FRACTION(): + return 10 ** 8 + + @staticmethod + def MAX_VALUE(): + return (2 ** 53) - 1 + + def __init__(self, currency, value=0, fraction=0): + # type: (str, int, int) -> Amount + assert(value >= 0 and fraction >= 0) + self.value = value + self.fraction = fraction + self.currency = currency + self.__normalize() + assert(self.value <= Amount.MAX_VALUE()) + + # Normalize amount + def __normalize(self): + if self.fraction >= Amount.FRACTION(): + self.value += int(self.fraction / Amount.FRACTION()) + self.fraction = self.fraction % Amount.FRACTION() + + # Parse a string matching the format "A:B.C" + # instantiating an amount object. + @classmethod + def parse(cls, amount_str): + exp = '^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$' + import re + parsed = re.search(exp, amount_str) + if not parsed: + raise BadAmount(amount_str) + value = int(parsed.group(2)) + fraction = 0 + for i, digit in enumerate(parsed.group(3)): + fraction += int(int(digit) * (Amount.FRACTION() / 10 ** (i+1))) + return cls(parsed.group(1), value, fraction) + + # Comare two amounts, return: + # -1 if a < b + # 0 if a == b + # 1 if a > b + @staticmethod + def cmp(a, b): + assert a.currency == b.currency + if a.value == b.value: + if a.fraction < b.fraction: + return -1 + if a.fraction > b.fraction: + return 1 + return 0 + if a.value < b.value: + return -1 + return 1 + + # Add the given amount to this one + def add(self, a): + assert self.currency == a.currency + self.value += a.value + self.fraction += a.fraction + self.__normalize() + + # Subtract passed amount from this one + def subtract(self, a): + assert self.currency == a.currency + if self.fraction < a.fraction: + self.fraction += Amount.FRACTION() + self.value -= 1 + if self.value < a.value: + raise ValueError('self is lesser than amount to be subtracted') + self.value -= a.value + self.fraction -= a.fraction + + # Dump string from this amount, will put 'ndigits' numbers + # after the dot. + def stringify(self, ndigits): + assert ndigits > 0 + ret = '%s:%s.' % (self.currency, str(self.value)) + f = self.fraction + for i in range(0, ndigits): + ret += str(int(f / (Amount.FRACTION() / 10))) + f = (f * 10) % (Amount.FRACTION()) + return ret + + # Dump the Taler-compliant 'dict' amount + def dump(self): + return dict(value=self.value, + fraction=self.fraction, + currency=self.currency) diff --git a/payments/taler/test_amount.py b/payments/taler/test_amount.py new file mode 100644 index 0000000..bd59c74 --- /dev/null +++ b/payments/taler/test_amount.py @@ -0,0 +1,63 @@ +# This file is part of TALER +# (C) 2017 Taler Systems SA +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci +# @version 0.0 +# @repository https://git.taler.net/copylib.git/ +# This code is "copylib", it is versioned under the Git repository +# mentioned above, and it is meant to be manually copied into any project +# which might need it. + +from __future__ import unicode_literals +from .amount import Amount, BadAmount +from unittest import TestCase +import json +from mock import MagicMock + +class TestAmount(TestCase): + def setUp(self): + self.amount = Amount('TESTKUDOS') + + def test_parse_and_cmp(self): + a = self.amount.parse('TESTKUDOS:0.0') + self.assertEqual(Amount.cmp(self.amount, a), 0) + b = self.amount.parse('TESTKUDOS:0.1') + self.assertEqual(Amount.cmp(Amount('TESTKUDOS', fraction=10000000), b), 0) + c = self.amount.parse('TESTKUDOS:3.3') + self.assertEqual(Amount.cmp(Amount('TESTKUDOS', 3, 30000000), c), 0) + self.assertEqual(Amount.cmp(a, b), -1) + self.assertEqual(Amount.cmp(c, b), 1) + with self.assertRaises(BadAmount): + Amount.parse(':3') + + def test_add_and_dump(self): + mocky = MagicMock() + self.amount.add(Amount('TESTKUDOS', 9, 10**8)) + mocky(**self.amount.dump()) + mocky.assert_called_with(currency='TESTKUDOS', value=10, fraction=0) + + def test_subtraction(self): + with self.assertRaises(ValueError): + self.amount.subtract(Amount('TESTKUDOS', fraction=1)) + a = Amount('TESTKUDOS', 2) + a.subtract(Amount('TESTKUDOS', 1, 99999999)) + self.assertEqual(Amount.cmp(a, Amount('TESTKUDOS', fraction=1)), 0) + + def test_stringify(self): + self.assertEqual(self.amount.stringify(3), 'TESTKUDOS:0.000') + self.amount.add(Amount('TESTKUDOS', 2, 100)) + self.assertEqual(self.amount.stringify(6), 'TESTKUDOS:2.000001') diff --git a/payments/taler/test_taler.py b/payments/taler/test_taler.py new file mode 100644 index 0000000..e2a5ce2 --- /dev/null +++ b/payments/taler/test_taler.py @@ -0,0 +1,155 @@ +# This file is part of DJANGO-PAYMENTS +# (C) 2017 Taler Systems SA +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci + +from __future__ import unicode_literals +import json +from unittest import TestCase +from . import TalerProvider +from mock import patch, MagicMock, Mock +from .. import RedirectNeeded, PaymentStatus +from decimal import Decimal + +class Payment(Mock): + status = PaymentStatus.WAITING + message = 'mocked payment' + total = Decimal(10) + description = 'mocked payment object' + instance = 'mock instance' + address = 'mock address' + name = 'mock name' + jurisdiction = 'mock jurisdiction' + currency = 'MOCK' + + def get_process_url(self): + return 'mock-process-url' + + def get_success_url(self): + return 'http://example.com/mock-success' + + def change_status(self, status): + self.status = status + +class TestTalerProvider(TestCase): + + def setUp(self): + self.payment = Payment() + self.provider = TalerProvider(backend_url='http://mocked_backend_url', + instance='mock_instance', address='mock address', + name='mock name', jurisdiction='mock jurisdiction') + + # This tests the very first redirect, then one that + # redirects to the first 402 page. + def test_provider_redirects_to_contract_url(self): + with self.assertRaises(RedirectNeeded): + self.provider.get_form(self.payment) + + # Test whether a contract generation url is returned + # along with the 402 status code. + def test_provider_contract_generation_header(self): + response = self.provider.process_data(self.payment, MagicMock()) + self.assertTrue(response.get('X-Taler-Contract-Url')) + self.assertFalse(response.get('X-Taler-Offer-Url')) + self.assertEqual(response.status_code, 402) + self.assertEqual(self.payment.status, PaymentStatus.INPUT) + + # Test whether the frontend logic interacts well with backend. + @patch('requests.post') + def test_provider_contract_generation(self, mocked_post): + request = MagicMock() + request.GET = {'nonce': 'nonce098'} + data = MagicMock() + data.return_value = {'not': 'changed'} + post = MagicMock() + post.status_code = 200 + post.json = data + mocked_post.return_value = post + self.payment.change_status(PaymentStatus.INPUT) + r = self.provider.process_data(self.payment, request) + total_amount = { + 'value': 10, + 'fraction': 0, + 'currency': self.payment.currency} + expected_order = {'order': { + 'summary': self.payment.message, + 'nonce': 'nonce098', + 'amount': total_amount, + 'products': [{ + 'description': self.payment.description, + 'quantity': 1, + 'product_id': 0, + 'price': total_amount}], + 'fulfillment_url': 'https://example.com/mock-process-url', + 'pay_url': 'https://example.com/mock-process-url', + 'merchant': { + 'instance': self.provider.instance, + 'address': self.provider.address, + 'name': self.provider.name, + 'jurisdiction': self.provider.jurisdiction}, + 'extra': {}}} + mocked_post.assert_called_with('http://mocked_backend_url/proposal', + json=expected_order) + self.assertEqual(self.payment.status, PaymentStatus.PREAUTH) + self.assertEqual(r.status_code, 200) + self.assertEqual(json.dumps(data.return_value), r.content.decode('utf-8')) + + # Testing second returned 402, the one returned by GETting the + # process url having the PREAUTH status. This logic is triggered + # rigth after the user confirms the payment. + def test_provider_trigger_payment(self): + request = MagicMock() + request.method = 'GET' + self.payment.change_status(PaymentStatus.PREAUTH) + r = self.provider.process_data(self.payment, request) + self.assertEqual(r.status_code, 402) + self.assertTrue(r.get('X-Taler-Contract-Url')) + self.assertTrue(r.get('X-Taler-Offer-Url')) + + # Testing coins receiving logic. + @patch('requests.post') + def test_provider_receive_payment(self, mocked_post): + data = MagicMock() + data.return_value = {'not': 'changed'} + post = MagicMock() + post.status_code = 200 + post.json = data + mocked_post.return_value = post + self.payment.change_status(PaymentStatus.PREAUTH) + request = MagicMock() + request.method = 'POST' + request.body = b'{"mock": "coins"}' + r = self.provider.process_data(self.payment, request) + self.assertEqual(r.status_code, 200) + self.assertEqual(self.payment.status, PaymentStatus.CONFIRMED) + mocked_post.assert_called_with('http://mocked_backend_url/pay', + json={'mock': 'coins'}) + self.assertEqual(json.dumps(data.return_value), r.content.decode('utf-8')) + + # Test whether successful payment gets redirected to + # "success url" + @patch('payments.taler.redirect') + def test_provider_success_payment(self, mocked_redirect): + self.payment.change_status(PaymentStatus.CONFIRMED) + self.provider.process_data(self.payment, MagicMock()) + mocked_redirect.assert_called_with('http://example.com/mock-success') + + # Test unknown payment status, should never happen + def test_provider_unknown_payment_status(self): + self.payment.change_status("does not exist") + r = self.provider.process_data(self.payment, MagicMock()) + self.assertEqual(r.status_code, 500) diff --git a/payments/templates/payments/taler_fallback.html b/payments/templates/payments/taler_fallback.html new file mode 100644 index 0000000..e84170a --- /dev/null +++ b/payments/templates/payments/taler_fallback.html @@ -0,0 +1,8 @@ + +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} + + + {% trans "Payment unsuccessful" %} + {{ msg }} + -- cgit v1.2.3