summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcello Stanisci <stanisci.m@gmail.com>2017-11-03 11:00:39 +0100
committerMarcello Stanisci <stanisci.m@gmail.com>2017-11-03 11:15:06 +0100
commita2edc40a47ec4cb45426599692f5f7e7c222d710 (patch)
tree1df52b5c21fe50f496c3616cb0a17e62ec34372f
parent49c93aa8d0dc4bba74a14a048a8ad5a011297998 (diff)
downloaddjango-payments-taler-a2edc40a47ec4cb45426599692f5f7e7c222d710.tar.gz
django-payments-taler-a2edc40a47ec4cb45426599692f5f7e7c222d710.tar.bz2
django-payments-taler-a2edc40a47ec4cb45426599692f5f7e7c222d710.zip
Adding the Taler payment system.
-rw-r--r--doc/modules.rst29
-rw-r--r--payments/locale/en/LC_MESSAGES/django.po24
-rw-r--r--payments/locale/it/LC_MESSAGES/django.po24
-rw-r--r--payments/locale/ru/LC_MESSAGES/django.po20
-rw-r--r--payments/taler/__init__.py156
-rw-r--r--payments/taler/amount.py120
-rw-r--r--payments/taler/test_amount.py63
-rw-r--r--payments/taler/test_taler.py155
-rw-r--r--payments/templates/payments/taler_fallback.html8
9 files changed, 589 insertions, 10 deletions
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 <https://taler/net/>`_.
+
+ :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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 <marco.badan@gmail.com>, 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 <weronika@mirumee.com>, 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 @@
+<!DOCTYPE html>
+{% load i18n %}
+{% get_current_language as LANGUAGE_CODE %}
+
+<html lang="{{ LANGUAGE_CODE }}">
+ <title>{% trans "Payment unsuccessful" %}</title>
+ <body>{{ msg }}</body>
+</html>