From 88f3a5ca208775268f676baff95f161aabc3eff4 Mon Sep 17 00:00:00 2001
From: Thomas NOEL <tnoel@entrouvert.com>
Date: Thu, 25 Jul 2019 15:20:41 +0200
Subject: [PATCH] lingo: accept french amount format (#35052)

---
 combo/apps/lingo/views.py   | 45 +++++++++++++++++++++++++------------
 tests/test_lingo_payment.py | 28 ++++++++++++++++++++++-
 2 files changed, 58 insertions(+), 15 deletions(-)

diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py
index 4150d79..70ed64b 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -26,7 +26,7 @@ from django.core.urlresolvers import reverse
 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
 from django.http import HttpResponseForbidden, Http404, JsonResponse
 from django.template.response import TemplateResponse
-from django.utils import timezone, dateparse
+from django.utils import timezone, dateparse, six
 from django.utils.encoding import force_text
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import View, DetailView, ListView, TemplateView
@@ -72,6 +72,14 @@ def lingo_check_request_signature(request):
     return check_request_signature(request, keys=keys)
 
 
+class LocaleDecimal(Decimal):
+    # accept , instead of . for French users comfort
+    def __new__(cls, value="0", *args, **kwargs):
+        if isinstance(value, six.string_types) and settings.LANGUAGE_CODE.startswith('fr-'):
+            value = value.replace(',', '.')
+        return super(LocaleDecimal, cls).__new__(cls, value, *args, **kwargs)
+
+
 class RegiesApiView(ListView):
     model = Regie
 
@@ -94,9 +102,9 @@ class AddBasketItemApiView(View):
 
     def get_amount(self, amount):
         if isinstance(amount, list):
-            d = Decimal(sum([Decimal(a) for a in amount]))
+            d = Decimal(sum([LocaleDecimal(a) for a in amount]))
         else:
-            d = Decimal(amount)
+            d = LocaleDecimal(amount)
         return d.quantize(Decimal('0.01'), ROUND_HALF_UP)
 
     def post(self, request, *args, **kwargs):
@@ -111,13 +119,22 @@ class AddBasketItemApiView(View):
             raise Exception('missing amount parameter')
 
         item = BasketItem(amount=0)
-        item.amount = self.get_amount(request.GET.getlist('amount'))
+        try:
+            item.amount = self.get_amount(request.GET.getlist('amount'))
+        except ArithmeticError:
+            return HttpResponseBadRequest('invalid value for "amount" in query string')
 
         if request_body.get('amount'):
-            item.amount += self.get_amount(request_body['amount'])
+            try:
+                item.amount += self.get_amount(request_body['amount'])
+            except ArithmeticError:
+                return HttpResponseBadRequest('invalid value for "amount" in payload')
 
         if extra.get('amount'):
-            item.amount += self.get_amount(extra['amount'])
+            try:
+                item.amount += self.get_amount(extra['amount'])
+            except ArithmeticError:
+                return HttpResponseBadRequest('invalid value for "amount" in extra payload')
 
         if 'extra' in request_body:
             item.request_data = request_body.get('extra')
@@ -234,11 +251,11 @@ class ValidateTransactionApiView(View):
             raise Http404
 
         payment = get_eopayment_object(request, transaction.regie)
-        amount = request.GET['amount']
+        amount = LocaleDecimal(request.GET['amount'])
 
         logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id))
         try:
-            result = payment.backend.validate(Decimal(amount), transaction.bank_data)
+            result = payment.backend.validate(amount, transaction.bank_data)
         except eopayment.ResponseError as e:
             logger.error(u'failed in validation operation: %s', e)
             response = HttpResponse(content_type='application/json')
@@ -247,7 +264,7 @@ class ValidateTransactionApiView(View):
 
         logger.info(u'bank validation result: %r', result)
         operation = TransactionOperation(transaction=transaction,
-                kind='validation', amount=Decimal(amount), bank_result=result)
+                kind='validation', amount=amount, bank_result=result)
         operation.save()
 
         response = HttpResponse(content_type='application/json')
@@ -275,11 +292,11 @@ class CancelTransactionApiView(View):
             raise Http404
 
         payment = get_eopayment_object(request, transaction.regie)
-        amount = request.GET['amount']
+        amount = LocaleDecimal(request.GET['amount'])
 
         logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id))
         try:
-            result = payment.backend.cancel(Decimal(amount), transaction.bank_data)
+            result = payment.backend.cancel(amount, transaction.bank_data)
         except eopayment.ResponseError as e:
             logger.error(u'failed in cancel operation: %s', e)
             response = HttpResponse(content_type='application/json')
@@ -288,7 +305,7 @@ class CancelTransactionApiView(View):
 
         logger.info(u'bank cancellation result: %r', result)
         operation = TransactionOperation(transaction=transaction,
-                kind='cancellation', amount=Decimal(amount), bank_result=result)
+                kind='cancellation', amount=amount, bank_result=result)
         operation.save()
 
         response = HttpResponse(content_type='application/json')
@@ -710,11 +727,11 @@ class SelfInvoiceView(View):
         except SelfDeclaredInvoicePayment.DoesNotExist:
             raise Http404()
         invoice_id = request.GET.get('invoice-number', '')
-        invoice_amount = request.GET.get('invoice-amount', '').replace(',', '.')
+        invoice_amount = request.GET.get('invoice-amount', '')
         msg = None
         url = None
         try:
-            invoice_amount = Decimal(invoice_amount)
+            invoice_amount = LocaleDecimal(invoice_amount)
         except ArithmeticError:
             invoice_amount = '-'
             msg = _('Sorry, the provided amount is invalid.')
diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py
index eff7703..aaccf53 100644
--- a/tests/test_lingo_payment.py
+++ b/tests/test_lingo_payment.py
@@ -12,6 +12,7 @@ from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from django.core.wsgi import get_wsgi_application
 from django.conf import settings
+from django.test import override_settings
 from django.utils import timezone
 from django.utils.six.moves.urllib import parse as urlparse
 from django.contrib.messages.storage.session import SessionStorage
@@ -238,6 +239,31 @@ def test_add_amount_to_basket(app, key, regie, user):
     assert json.loads(resp.content)['result'] == 'success'
     assert BasketItem.objects.filter(amount=Decimal('81.22')).exists()
 
+    # accept french notation if settings.LANGUAGE_CODE is 'fr-*'
+    url = '%s?amount=10,00&email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email)
+    url = sign_url(url, key)
+    resp = app.post_json(url, params=data, status=400)
+    assert 'invalid value for "amount" in query string' in resp.content
+    data['amount'] = '1,10'
+    url = '%s?amount=10.00&email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email)
+    url = sign_url(url, key)
+    resp = app.post_json(url, params=data, status=400)
+    assert 'invalid value for "amount" in payload' in resp.content
+    data['amount'] = '1.10'
+    data['extra'] = {'amount': '0,01'}
+    resp = app.post_json(url, params=data, status=400)
+    assert 'invalid value for "amount" in extra payload' in resp.content
+
+    data['amount'] = '1,10'
+    data['extra'] = {'amount': '0,01'}
+    url = '%s?amount=10,00&email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email)
+    url = sign_url(url, key)
+    with override_settings(LANGUAGE_CODE='fr-be'):
+        resp = app.post_json(url, params=data, status=200)
+    assert resp.status_code == 200
+    assert json.loads(resp.content)['result'] == 'success'
+    assert BasketItem.objects.filter(amount=Decimal('11.11')).exists()
+
     other_regie.is_default = True
     other_regie.save()
     data['amount'] = []
@@ -408,7 +434,7 @@ def test_pay_multiple_regies(app, key, regie, user):
     resp = resp.forms[0].submit()
     assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
     qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
-    assert qs['amount'] == ['223.35']
+    assert qs['amount'] == ['234.46']
 
     resp = login(app).get(page.get_online_url())
     resp = resp.forms[1].submit()
-- 
2.20.1

