Projet

Général

Profil

0001-lingo-add-possibility-to-compute-extra-fees-16065.patch

Frédéric Péters, 28 mai 2017 14:31

Télécharger (13,1 ko)

Voir les différences:

Subject: [PATCH] lingo: add possibility to compute extra fees (#16065)

 .../lingo/migrations/0029_auto_20170528_1334.py    | 34 ++++++++++
 combo/apps/lingo/models.py                         | 44 +++++++++++++
 combo/apps/lingo/templates/lingo/combo/basket.html |  2 +-
 combo/apps/lingo/views.py                          |  8 +++
 tests/test_lingo_payment.py                        | 74 +++++++++++++++++++++-
 5 files changed, 160 insertions(+), 2 deletions(-)
 create mode 100644 combo/apps/lingo/migrations/0029_auto_20170528_1334.py
combo/apps/lingo/migrations/0029_auto_20170528_1334.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
import jsonfield.fields
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('lingo', '0028_tipipaymentformcell'),
12
    ]
13

  
14
    operations = [
15
        migrations.AlterModelOptions(
16
            name='basketitem',
17
            options={'ordering': ['regie', 'extra_fee', 'subject']},
18
        ),
19
        migrations.AddField(
20
            model_name='basketitem',
21
            name='extra_fee',
22
            field=models.BooleanField(default=False),
23
        ),
24
        migrations.AddField(
25
            model_name='basketitem',
26
            name='request_data',
27
            field=jsonfield.fields.JSONField(default=dict, blank=True),
28
        ),
29
        migrations.AddField(
30
            model_name='regie',
31
            name='extra_fees_ws_url',
32
            field=models.URLField(verbose_name='Webservice URL to compute extra fees', blank=True),
33
        ),
34
    ]
combo/apps/lingo/models.py
83 83
    is_default = models.BooleanField(verbose_name=_('Default Regie'), default=False)
84 84
    webservice_url = models.URLField(_('Webservice URL to retrieve remote items'),
85 85
                        blank=True)
86
    extra_fees_ws_url = models.URLField(_('Webservice URL to compute extra fees'),
87
            blank=True)
86 88
    payment_min_amount = models.DecimalField(_('Minimal payment amount'),
87 89
                            max_digits=7, decimal_places=2, default=0)
88 90

  
......
167 169
                'text': self.label,
168 170
                'description': self.description}
169 171

  
172
    def compute_extra_fees(self, user):
173
        if not self.extra_fees_ws_url:
174
            return
175
        post_data = {'data': []}
176
        basketitems = BasketItem.objects.filter(
177
                user=user, regie=self,
178
                cancellation_date__isnull=True,
179
                payment_date__isnull=True)
180
        for basketitem in basketitems.filter(extra_fee=False):
181
            basketitem_data = {
182
                'subject': basketitem.subject,
183
                'source_url': basketitem.source_url,
184
                'details': basketitem.details,
185
                'amount': basketitem.amount,
186
                'request_data': basketitem.request_data
187
            }
188
            post_data['data'].append(basketitem_data)
189
        if not post_data['data']:
190
            basketitems.filter(extra_fee=True).delete()
191
            return
192
        response = requests.post(self.extra_fees_ws_url, remote_service='auto')
193
        if response.status_code != 200 or response.json().get('err'):
194
            logger = logging.getLogger(__name__)
195
            logger.error('failed to compute extra frees (user: %r)', user)
196
            return
197
        basketitems.filter(extra_fee=True).delete()
198
        for extra_fee in response.json().get('data'):
199
            BasketItem(user=user, regie=self,
200
                    subject=extra_fee.get('subject'),
201
                    amount=extra_fee.get('amount'),
202
                    extra_fee=True,
203
                    user_cancellable=False).save()
204

  
170 205

  
171 206
class BasketItem(models.Model):
172 207
    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
......
176 211
    details = models.TextField(verbose_name=_('Details'), blank=True)
177 212
    amount = models.DecimalField(verbose_name=_('Amount'),
178 213
            decimal_places=2, max_digits=8)
214
    request_data = JSONField(blank=True)
215
    extra_fee = models.BooleanField(default=False)
179 216
    user_cancellable = models.BooleanField(default=True)
180 217
    creation_date = models.DateTimeField(auto_now_add=True)
181 218
    cancellation_date = models.DateTimeField(null=True)
182 219
    payment_date = models.DateTimeField(null=True)
183 220
    notification_date = models.DateTimeField(null=True)
184 221

  
222
    class Meta:
223
        ordering = ['regie', 'extra_fee', 'subject']
224

  
185 225
    def notify(self, status):
226
        if not self.source_url:
227
            return
186 228
        url = self.source_url + 'jump/trigger/%s' % status
187 229
        message = {'result': 'ok'}
188 230
        if status == 'paid':
......
200 242
        self.notify('paid')
201 243
        self.notification_date = timezone.now()
202 244
        self.save()
245
        self.regie.compute_extra_fees(user=self.user)
203 246

  
204 247
    def notify_cancellation(self, notify_origin=False):
205 248
        if notify_origin:
206 249
            self.notify('cancelled')
207 250
        self.cancellation_date = timezone.now()
208 251
        self.save()
252
        self.regie.compute_extra_fees(user=self.user)
209 253

  
210 254
    @property
211 255
    def total_amount(self):
combo/apps/lingo/templates/lingo/combo/basket.html
8 8
<input type="hidden" name="next_url" value="{{ cell.page.get_online_url }}" />
9 9
<ul>
10 10
  {% for item in regie_info.items %}
11
  <li><a href="{{ item.source_url }}">{{ item.subject }}</a>: {{ item.amount }} €
11
  <li><a {% if item.source_url %}href="{{ item.source_url }}{% endif %}">{{ item.subject }}</a>: {{ item.amount }} €
12 12
          {% if item.user_cancellable %}
13 13
          <a rel="popup" href="{% url 'lingo-cancel-item' pk=item.id %}">({% trans 'remove' %})</a>
14 14
          {% endif %}
combo/apps/lingo/views.py
114 114
        if extra.get('amount'):
115 115
            item.amount += self.get_amount(extra['amount'])
116 116

  
117
        if 'extra' in request_body:
118
            item.request_data = request_body.get('extra')
119
        else:
120
            item.request_data = request_body
121

  
117 122
        try:
118 123
            if request.GET.get('NameId'):
119 124
                if UserSAMLIdentifier is None:
......
153 158
        item.source_url = request_body.get('url') or ''
154 159

  
155 160
        item.save()
161
        item.regie.compute_extra_fees(user=item.user)
156 162

  
157 163
        response = HttpResponse(content_type='application/json')
158 164
        response.write(json.dumps({'result': 'success', 'id': str(item.id)}))
......
297 303
                    remote_items_data.append(regie.get_invoice(request.user, item_id))
298 304
                remote_items = ','.join([x.id for x in remote_items_data])
299 305
            else:
306
                regie.compute_extra_fees(user=self.request.user)
300 307
                items = BasketItem.objects.filter(user=self.request.user,
301 308
                        regie=regie, payment_date__isnull=True,
302 309
                        cancellation_date__isnull=True)
......
427 434
            except RuntimeError:
428 435
                # ignore errors, it should be retried later on if it fails
429 436
                pass
437
        regie.compute_extra_fees(user=transaction.user)
430 438
        if transaction.remote_items:
431 439
            for item_id in transaction.remote_items.split(','):
432 440
                remote_item = regie.get_invoice(user=transaction.user, invoice_id=item_id)
tests/test_lingo_payment.py
191 191

  
192 192
    url = '%s?email=%s&regie_id=%s' % (
193 193
            reverse('api-add-basket-item'), user_email, regie.id)
194
    data['extra'] = {'amount': '22.24'}
194
    data['extra'] = {'amount': '22.24', 'foo': 'bar'}
195 195
    url = sign_url(url, settings.LINGO_API_SIGN_KEY)
196 196
    resp = client.post(url, json.dumps(data), content_type='application/json')
197 197
    assert resp.status_code == 200
198 198
    assert json.loads(resp.content)['result'] == 'success'
199 199
    assert BasketItem.objects.filter(amount=Decimal('22.24')).exists()
200 200
    assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].regie_id == regie.id
201
    assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].request_data == data['extra']
201 202

  
202 203
    url = '%s?email=%s&regie_id=%s' % (
203 204
        reverse('api-add-basket-item'), user_email, regie.slug)
......
446 447
        resp = client.post(url, content_type='application/json')
447 448
        assert json.loads(resp.content)['err'] == 1
448 449
        assert TransactionOperation.objects.filter(transaction=t1).count() == 1
450

  
451
def test_extra_fees(key, regie, user):
452
    regie.extra_fees_ws_url = 'http://www.example.net/extra-fees'
453
    regie.save()
454

  
455
    user_email = 'foo@example.com'
456
    User.objects.get_or_create(email=user_email)
457
    amount = 42
458
    data = {'amount': amount, 'display_name': 'test amount'}
459
    with mock.patch('combo.utils.RequestsSession.request') as request:
460
        mock_json = mock.Mock()
461
        mock_json.status_code = 200
462
        mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '5'}]}
463
        request.return_value = mock_json
464
        url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key)
465
        resp = client.post(url, json.dumps(data), content_type='application/json')
466
    assert resp.status_code == 200
467
    assert json.loads(resp.content)['result'] == 'success'
468
    assert BasketItem.objects.filter(amount=amount).exists()
469
    assert BasketItem.objects.filter(amount=amount)[0].regie_id == regie.id
470
    assert BasketItem.objects.filter(amount=5, extra_fee=True).exists()
471
    assert BasketItem.objects.filter(amount=5, extra_fee=True)[0].regie_id == regie.id
472

  
473
    with mock.patch('combo.utils.RequestsSession.request') as request:
474
        mock_json = mock.Mock()
475
        mock_json.status_code = 200
476
        mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '7'}]}
477
        request.return_value = mock_json
478
        data['amount'] = 43
479
        url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key)
480
        resp = client.post(url, json.dumps(data), content_type='application/json')
481
    assert resp.status_code == 200
482
    assert json.loads(resp.content)['result'] == 'success'
483
    assert not BasketItem.objects.filter(amount=5, extra_fee=True).exists()
484
    assert BasketItem.objects.filter(amount=7, extra_fee=True).exists()
485

  
486
    with mock.patch('combo.utils.RequestsSession.request') as request:
487
        mock_json = mock.Mock()
488
        mock_json.status_code = 200
489
        mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '4'}]}
490
        request.return_value = mock_json
491
        url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_email), key)
492
        data = {'basket_item_id': BasketItem.objects.get(amount=43).id}
493
        resp = client.post(url, json.dumps(data), content_type='application/json')
494
    assert resp.status_code == 200
495
    assert not BasketItem.objects.filter(amount=7, extra_fee=True).exists()
496
    assert BasketItem.objects.filter(amount=4, extra_fee=True).exists()
497

  
498
    # test payment
499
    login()
500

  
501
    with mock.patch('combo.utils.RequestsSession.request') as request:
502
        mock_json = mock.Mock()
503
        mock_json.status_code = 200
504
        mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '2'}]}
505
        request.return_value = mock_json
506
        resp = client.post(reverse('lingo-pay'), {'regie': regie.pk})
507
    assert resp.status_code == 302
508
    location = resp.get('location')
509
    parsed = urlparse.urlparse(location)
510
    qs = urlparse.parse_qs(parsed.query)
511
    transaction_id = qs['transaction_id'][0]
512
    data = {'transaction_id': transaction_id, 'signed': True,
513
            'amount': qs['amount'][0], 'ok': True}
514
    assert data['amount'] == '44.00'
515

  
516
    # call callback with GET
517
    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
518
    resp = client.get(callback_url, data)
519
    assert resp.status_code == 200
520
    assert Transaction.objects.get(order_id=transaction_id).status == 3
449
-