Projet

Général

Profil

0001-lingo-check-real-payment-status-of-remote_item-when-.patch

Benjamin Dauvergne, 15 avril 2021 21:24

Télécharger (12,7 ko)

Voir les différences:

Subject: [PATCH] lingo: check real payment status of remote_item when shown or
 paid (#53186)

Currently the remote_item.paid attribute comes from what the remote
regie is providing through a web service. But a remote_item can be
already paid if a transaction with a paid status (eopayment.PAID or
eopayment.ACCEPTED) already exist.

This patch add a RemoteItem.update_paid(regie, remote_items) which
update bunch of remote_item(s) by querying the correponding paid
transactions.

The methods Regie.get_invoices() and Regie.get_invoice() get a new
parameter update_paid=False, which is set to true in cells and views
which display or allow paying a remote item.
 combo/apps/lingo/models.py       | 53 +++++++++++++++++++++++++++---
 combo/apps/lingo/views.py        | 10 ++++--
 tests/test_lingo_remote_regie.py | 55 ++++++++++++++++++++++----------
 3 files changed, 93 insertions(+), 25 deletions(-)
combo/apps/lingo/models.py
21 21
import logging
22 22
import re
23 23
from decimal import Decimal
24
from functools import reduce
24 25

  
25 26
import eopayment
26 27
from dateutil import parser
......
228 229
            return self.text_on_success
229 230
        return _('Your payment has been succesfully registered.')
230 231

  
231
    def get_invoices(self, user, history=False):
232
    def get_invoices(self, user, history=False, update_paid=False):
232 233
        if not self.is_remote():
233 234
            return []
234 235
        if user:
......
254 255
            if items.get('data'):
255 256
                if not isinstance(items['data'], list):
256 257
                    raise RegieException(regie_exc_msg)
257
                return [build_remote_item(item, self) for item in items['data']]
258
                remote_items = [build_remote_item(item, self) for item in items['data']]
259
                if not history and update_paid:
260
                    # update paid status using known transactions
261
                    RemoteItem.update_paid(self, remote_items)
262
                return remote_items
258 263
            return []
259 264
        return []
260 265

  
261
    def get_invoice(self, user, invoice_id, log_errors=True, raise_4xx=False):
266
    def get_invoice(self, user, invoice_id, log_errors=True, raise_4xx=False, update_paid=False):
262 267
        if not self.is_remote():
263 268
            return self.basketitem_set.get(pk=invoice_id)
264 269
        url = self.webservice_url + '/invoice/%s/' % invoice_id
......
274 279
            raise RemoteInvoiceException()
275 280
        if response.json().get('data') is None:
276 281
            raise ObjectDoesNotExist()
277
        return build_remote_item(response.json().get('data'), self)
282
        remote_item = build_remote_item(response.json().get('data'), self)
283
        if update_paid:
284
            # update paid status using known transactions
285
            RemoteItem.update_paid(self, [remote_item])
286
        return remote_item
278 287

  
279 288
    def get_invoice_pdf(self, user, invoice_id):
280 289
        """
......
603 612
    def crypto_id(self):
604 613
        return aes_hex_encrypt(settings.SECRET_KEY, force_bytes(str(self.id)))
605 614

  
615
    @classmethod
616
    def update_paid(cls, regie, remote_items):
617
        remote_item_ids = [remote_item.id for remote_item in remote_items if not remote_item.paid]
618
        if not remote_item_ids:
619
            return
620

  
621
        paid_items = {}
622
        # filter transactions by regie, status and contained remote_item id
623
        transaction_qs = Transaction.objects.filter(
624
            regie=regie, status__in=[eopayment.PAID, eopayment.ACCEPTED]
625
        )
626
        query = reduce(
627
            models.Q.__or__,
628
            (models.Q(remote_items__contains=remote_item_id) for remote_item_id in remote_item_ids),
629
        )
630

  
631
        # accumulate in paid_items each remote_item earliest payment_date
632
        for transaction in transaction_qs.filter(query):
633
            for remote_item in transaction.remote_items.split(','):
634
                if remote_item not in paid_items:
635
                    paid_items[remote_item] = transaction.end_date
636
                else:
637
                    paid_items[remote_item] = min(transaction.end_date, paid_items[remote_item])
638

  
639
        # update remote_item.paid using paid_items
640
        for remote_item in remote_items:
641
            if remote_item.paid:
642
                continue
643
            if remote_item.id in paid_items:
644
                remote_item.paid = True
645
                remote_item.payment_date = paid_items[remote_item.id]
646

  
606 647

  
607 648
class Transaction(models.Model):
608 649
    regie = models.ForeignKey(Regie, on_delete=models.CASCADE, null=True)
......
915 956
        errors = []
916 957
        for r in self.get_regies():
917 958
            try:
918
                items.extend(r.get_invoices(user))
959
                for remote_item in r.get_invoices(user, update_paid=True):
960
                    if not remote_item.paid:
961
                        items.append(remote_item)
919 962
            except RegieException as e:
920 963
                errors.append(e)
921 964
        return items, errors
combo/apps/lingo/views.py
390 390
            messages.error(request, _('This regie allows to pay only one item.'))
391 391
            return HttpResponseRedirect(next_url)
392 392

  
393
        if any(item.paid for item in remote_items):
394
            messages.error(request, _('Some items are already paid.'))
395
            return HttpResponseRedirect(next_url)
396

  
393 397
        total_amount = sum([x.amount for x in remote_items or items])
394 398

  
395 399
        if total_amount < regie.payment_min_amount:
......
497 501
                regie = Regie.objects.get(pk=regie_id)
498 502
                # get all items data from regie webservice
499 503
                for item_id in request.POST.getlist('item'):
500
                    remote_items.append(regie.get_invoice(user, item_id))
504
                    remote_items.append(regie.get_invoice(user, item_id, update_paid=True))
501 505
            except (requests.exceptions.RequestException, RemoteInvoiceException):
502 506
                messages.error(request, _(u'Technical error: impossible to retrieve invoices.'))
503 507
                return HttpResponseRedirect(next_url)
......
878 882
            raise Http404()
879 883

  
880 884
        try:
881
            item = regie.get_invoice(self.request.user, item_id)
885
            item = regie.get_invoice(self.request.user, item_id, update_paid=True)
882 886
            if self.request.GET.get('page'):
883 887
                try:
884 888
                    ret['page'] = Page.objects.get(pk=self.request.GET['page'])
......
948 952
        else:
949 953
            for regie in obj.get_regies():
950 954
                try:
951
                    invoice = regie.get_invoice(None, invoice_id, log_errors=False)
955
                    invoice = regie.get_invoice(None, invoice_id, log_errors=False, update_paid=True)
952 956
                except ObjectDoesNotExist:
953 957
                    continue
954 958
                if invoice.total_amount != invoice_amount:
tests/test_lingo_remote_regie.py
4 4
import json
5 5
from decimal import Decimal
6 6

  
7
import eopayment
7 8
import mock
8 9
import pytest
9 10
from django.apps import apps
......
47 48
        'has_pdf': True,
48 49
        'online_payment': True,
49 50
        'paid': False,
50
        'payment_date': '1970-01-01',
51
        'payment_date': None,
51 52
        'no_online_payment_reason': '',
52 53
        'reference_id': 'order-id-1',
53 54
    },
......
63 64
        'has_pdf': True,
64 65
        'online_payment': True,
65 66
        'paid': False,
66
        'payment_date': '1970-01-01',
67
        'payment_date': None,
67 68
        'no_online_payment_reason': '',
68 69
        'reference_id': 'order-id-2',
69 70
    },
......
140 141
    assert 'F-2016-Two' in content
141 142
    assert '543.21' in content
142 143

  
144
    # set the second one as paid
145
    Transaction.objects.create(
146
        regie=remote_regie, remote_items=INVOICES[1]['id'], status=eopayment.PAID, end_date=now()
147
    )
148
    content = cell.render(context)
149
    assert 'F-2016-One' in content
150
    assert '123.45' in content
151
    assert 'F-2016-Two' not in content
152
    assert '543.21' not in content
153

  
143 154
    assert '?page=%s' % page.pk in content
144 155
    # check if regie webservice has been correctly called
145 156
    assert mock_send.call_args[0][0].method == 'GET'
......
367 378
    assert 'Total amount: <span class="amount">123.45€</span>' in resp.text
368 379
    assert 'Amount to pay: <span class="amount">123.45€</span>' in resp.text
369 380
    assert 'Amount already paid>' not in resp.text
381
    assert '"buttons"' in resp
370 382

  
371 383
    form = resp.form
372 384

  
......
424 436

  
425 437
    assert resp.status_code == 200
426 438

  
439
    # check invoice cannot be paid a second time
440
    resp = app.get('/lingo/item/%s/%s/' % (remote_regie.id, encrypt_id))
441
    assert '"buttons"' not in resp
442

  
443
    resp = form.submit()
444
    assert resp.location == '/'
445
    assert 'Some items are already paid' in app.session['_messages']
446

  
427 447

  
428 448
@mock.patch('combo.apps.lingo.models.requests.get')
429 449
def test_remote_item_failure(mock_get, app, remote_regie):
......
626 646
    appconfig.update_transactions()
627 647

  
628 648

  
649
@pytest.mark.parametrize('can_pay_only_one_basket_item', [False, True])
629 650
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
630 651
@mock.patch('combo.apps.lingo.models.requests.get')
631
def test_remote_invoice_successfull_payment_redirect(mock_get, mock_pay_invoice, app, remote_regie):
652
def test_remote_invoice_successfull_payment_redirect(
653
    mock_get, mock_pay_invoice, can_pay_only_one_basket_item, app, remote_regie
654
):
632 655
    assert remote_regie.is_remote()
633
    assert remote_regie.can_pay_only_one_basket_item is False
656
    remote_regie.can_pay_only_one_basket_item = can_pay_only_one_basket_item
634 657
    remote_regie.save()
635 658

  
636 659
    page = Page(title='xxx', slug='active-remote-invoices-page', template_name='standard')
......
641 664
    mock_get.return_value = mock_json
642 665
    mock_pay_invoice.return_value = mock.Mock(status_code=200)
643 666
    resp = app.get('/lingo/item/%s/%s/?page=%s' % (remote_regie.id, encrypt_id, page.pk))
667
    assert '"paid"' not in resp
644 668
    form = resp.form
645 669
    assert form['next_url'].value == '/active-remote-invoices-page/'
646 670
    form['email'] = 'test@example.net'
......
652 676
    parsed = urlparse.urlparse(location)
653 677
    # get return_url and transaction id from location
654 678
    qs = urlparse.parse_qs(parsed.query)
655
    assert 'orderid' not in qs
656
    assert 'subject' not in qs
679
    if can_pay_only_one_basket_item:
680
        assert qs['orderid'] == ['order-id-1']
681
        assert qs['subject'] == ['invoice-one']
682
    else:
683
        assert 'orderid' not in qs
684
        assert 'subject' not in qs
657 685
    args = {'transaction_id': qs['transaction_id'][0], 'signed': True, 'ok': True, 'reason': 'Paid'}
658 686
    resp = app.get(qs['return_url'][0], params=args)
659 687
    # redirect to payment status
......
665 693
        == '/active-remote-invoices-page/'
666 694
    )
667 695

  
668
    # one item limitation: send orderid to eopayment
669
    remote_regie.can_pay_only_one_basket_item = True
670
    remote_regie.save()
671
    resp = form.submit()
672
    assert resp.status_code == 302
673
    location = resp.location
674
    assert 'dummy-payment' in location
675
    parsed = urlparse.urlparse(location)
676
    qs = urlparse.parse_qs(parsed.query)
677
    assert qs['orderid'] == ['order-id-1']
678
    assert qs['subject'] == ['invoice-one']
696
    # check true payment status is visible, even if the remote regie web-service still report the invoice as unpaid
697
    resp = app.get('/lingo/item/%s/%s/?page=%s' % (remote_regie.id, encrypt_id, page.pk))
698
    assert not INVOICES[0]['paid']
699
    assert '"paid"' in resp
679 700

  
680 701

  
681 702
@mock.patch('combo.apps.lingo.models.UserSAMLIdentifier')
682
-