lingo: add possibility to compute extra fees (#16065)

This commit is contained in:
Frédéric Péters 2017-05-28 12:47:24 +02:00
parent a013add9ec
commit 23b7b294eb
5 changed files with 160 additions and 2 deletions

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('lingo', '0028_tipipaymentformcell'),
]
operations = [
migrations.AlterModelOptions(
name='basketitem',
options={'ordering': ['regie', 'extra_fee', 'subject']},
),
migrations.AddField(
model_name='basketitem',
name='extra_fee',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='basketitem',
name='request_data',
field=jsonfield.fields.JSONField(default=dict, blank=True),
),
migrations.AddField(
model_name='regie',
name='extra_fees_ws_url',
field=models.URLField(verbose_name='Webservice URL to compute extra fees', blank=True),
),
]

View File

@ -83,6 +83,8 @@ class Regie(models.Model):
is_default = models.BooleanField(verbose_name=_('Default Regie'), default=False)
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'),
blank=True)
extra_fees_ws_url = models.URLField(_('Webservice URL to compute extra fees'),
blank=True)
payment_min_amount = models.DecimalField(_('Minimal payment amount'),
max_digits=7, decimal_places=2, default=0)
@ -167,6 +169,39 @@ class Regie(models.Model):
'text': self.label,
'description': self.description}
def compute_extra_fees(self, user):
if not self.extra_fees_ws_url:
return
post_data = {'data': []}
basketitems = BasketItem.objects.filter(
user=user, regie=self,
cancellation_date__isnull=True,
payment_date__isnull=True)
for basketitem in basketitems.filter(extra_fee=False):
basketitem_data = {
'subject': basketitem.subject,
'source_url': basketitem.source_url,
'details': basketitem.details,
'amount': basketitem.amount,
'request_data': basketitem.request_data
}
post_data['data'].append(basketitem_data)
if not post_data['data']:
basketitems.filter(extra_fee=True).delete()
return
response = requests.post(self.extra_fees_ws_url, remote_service='auto')
if response.status_code != 200 or response.json().get('err'):
logger = logging.getLogger(__name__)
logger.error('failed to compute extra fees (user: %r)', user)
return
basketitems.filter(extra_fee=True).delete()
for extra_fee in response.json().get('data'):
BasketItem(user=user, regie=self,
subject=extra_fee.get('subject'),
amount=extra_fee.get('amount'),
extra_fee=True,
user_cancellable=False).save()
class BasketItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
@ -176,13 +211,20 @@ class BasketItem(models.Model):
details = models.TextField(verbose_name=_('Details'), blank=True)
amount = models.DecimalField(verbose_name=_('Amount'),
decimal_places=2, max_digits=8)
request_data = JSONField(blank=True)
extra_fee = models.BooleanField(default=False)
user_cancellable = models.BooleanField(default=True)
creation_date = models.DateTimeField(auto_now_add=True)
cancellation_date = models.DateTimeField(null=True)
payment_date = models.DateTimeField(null=True)
notification_date = models.DateTimeField(null=True)
class Meta:
ordering = ['regie', 'extra_fee', 'subject']
def notify(self, status):
if not self.source_url:
return
url = self.source_url + 'jump/trigger/%s' % status
message = {'result': 'ok'}
if status == 'paid':
@ -200,12 +242,14 @@ class BasketItem(models.Model):
self.notify('paid')
self.notification_date = timezone.now()
self.save()
self.regie.compute_extra_fees(user=self.user)
def notify_cancellation(self, notify_origin=False):
if notify_origin:
self.notify('cancelled')
self.cancellation_date = timezone.now()
self.save()
self.regie.compute_extra_fees(user=self.user)
@property
def total_amount(self):

View File

@ -8,7 +8,7 @@
<input type="hidden" name="next_url" value="{{ cell.page.get_online_url }}" />
<ul>
{% for item in regie_info.items %}
<li><a href="{{ item.source_url }}">{{ item.subject }}</a>: {{ item.amount }} €
<li><a {% if item.source_url %}href="{{ item.source_url }}{% endif %}">{{ item.subject }}</a>: {{ item.amount }} €
{% if item.user_cancellable %}
<a rel="popup" href="{% url 'lingo-cancel-item' pk=item.id %}">({% trans 'remove' %})</a>
{% endif %}

View File

@ -114,6 +114,11 @@ class AddBasketItemApiView(View):
if extra.get('amount'):
item.amount += self.get_amount(extra['amount'])
if 'extra' in request_body:
item.request_data = request_body.get('extra')
else:
item.request_data = request_body
try:
if request.GET.get('NameId'):
if UserSAMLIdentifier is None:
@ -153,6 +158,7 @@ class AddBasketItemApiView(View):
item.source_url = request_body.get('url') or ''
item.save()
item.regie.compute_extra_fees(user=item.user)
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'result': 'success', 'id': str(item.id)}))
@ -297,6 +303,7 @@ class PayView(View):
remote_items_data.append(regie.get_invoice(request.user, item_id))
remote_items = ','.join([x.id for x in remote_items_data])
else:
regie.compute_extra_fees(user=self.request.user)
items = BasketItem.objects.filter(user=self.request.user,
regie=regie, payment_date__isnull=True,
cancellation_date__isnull=True)
@ -427,6 +434,7 @@ class CallbackView(View):
except RuntimeError:
# ignore errors, it should be retried later on if it fails
pass
regie.compute_extra_fees(user=transaction.user)
if transaction.remote_items:
for item_id in transaction.remote_items.split(','):
remote_item = regie.get_invoice(user=transaction.user, invoice_id=item_id)

View File

@ -191,13 +191,14 @@ def test_add_amount_to_basket(key, regie, user):
url = '%s?email=%s&regie_id=%s' % (
reverse('api-add-basket-item'), user_email, regie.id)
data['extra'] = {'amount': '22.24'}
data['extra'] = {'amount': '22.24', 'foo': 'bar'}
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('22.24')).exists()
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].regie_id == regie.id
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].request_data == data['extra']
url = '%s?email=%s&regie_id=%s' % (
reverse('api-add-basket-item'), user_email, regie.slug)
@ -446,3 +447,74 @@ def test_transaction_cancel(key, regie, user):
resp = client.post(url, content_type='application/json')
assert json.loads(resp.content)['err'] == 1
assert TransactionOperation.objects.filter(transaction=t1).count() == 1
def test_extra_fees(key, regie, user):
regie.extra_fees_ws_url = 'http://www.example.net/extra-fees'
regie.save()
user_email = 'foo@example.com'
User.objects.get_or_create(email=user_email)
amount = 42
data = {'amount': amount, 'display_name': 'test amount'}
with mock.patch('combo.utils.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '5'}]}
request.return_value = mock_json
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key)
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content)['result'] == 'success'
assert BasketItem.objects.filter(amount=amount).exists()
assert BasketItem.objects.filter(amount=amount)[0].regie_id == regie.id
assert BasketItem.objects.filter(amount=5, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=5, extra_fee=True)[0].regie_id == regie.id
with mock.patch('combo.utils.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '7'}]}
request.return_value = mock_json
data['amount'] = 43
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key)
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content)['result'] == 'success'
assert not BasketItem.objects.filter(amount=5, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=7, extra_fee=True).exists()
with mock.patch('combo.utils.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '4'}]}
request.return_value = mock_json
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_email), key)
data = {'basket_item_id': BasketItem.objects.get(amount=43).id}
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert not BasketItem.objects.filter(amount=7, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=4, extra_fee=True).exists()
# test payment
login()
with mock.patch('combo.utils.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '2'}]}
request.return_value = mock_json
resp = client.post(reverse('lingo-pay'), {'regie': regie.pk})
assert resp.status_code == 302
location = resp.get('location')
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '44.00'
# call callback with GET
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
resp = client.get(callback_url, data)
assert resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3