0001-lingo-check-real-payment-status-of-remote_item-when-.patch
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 |
- |