0005-lingo-poll-running-transactions-status-in-cells-4914.patch
combo/apps/lingo/models.py | ||
---|---|---|
598 | 598 | |
599 | 599 |
@classmethod |
600 | 600 |
def get_items_to_be_paid(cls, user): |
601 |
return cls.objects.filter(
|
|
601 |
qs = cls.objects.filter(
|
|
602 | 602 |
user=user, payment_date__isnull=True, waiting_date__isnull=True, cancellation_date__isnull=True |
603 | 603 |
) |
604 |
for transaction in Transaction.objects.filter(items__in=qs): |
|
605 |
if transaction.can_poll_backend(): |
|
606 |
transaction.poll_backend() |
|
607 |
return qs |
|
604 | 608 | |
605 | 609 |
def notify(self, status): |
606 | 610 |
if not self.source_url: |
... | ... | |
682 | 686 |
self.reference_id = reference_id |
683 | 687 |
if payment_date: |
684 | 688 |
self.payment_date = parser.parse(payment_date) |
689 |
self.waiting_date = None |
|
685 | 690 | |
686 | 691 |
@property |
687 | 692 |
def no_online_payment_reason_details(self): |
... | ... | |
699 | 704 |
return aes_hex_encrypt(settings.SECRET_KEY, force_bytes(str(self.id))) |
700 | 705 | |
701 | 706 |
@classmethod |
702 |
def update_paid(cls, regie, remote_items):
|
|
703 |
remote_item_ids = [remote_item.id for remote_item in remote_items if not remote_item.paid]
|
|
707 |
def transactions_for_remote_items(cls, queryset, remote_items):
|
|
708 |
remote_item_ids = set(remote_item.id for remote_item in remote_items if not remote_item.paid)
|
|
704 | 709 |
if not remote_item_ids: |
705 |
return |
|
710 |
return Transaction.objects.none()
|
|
706 | 711 | |
707 |
paid_items = {} |
|
708 | 712 |
# filter transactions by regie, status and contained remote_item id |
709 |
transaction_qs = Transaction.objects.filter( |
|
710 |
regie=regie, status__in=[eopayment.PAID, eopayment.ACCEPTED] |
|
711 |
) |
|
712 | 713 |
query = reduce( |
713 | 714 |
models.Q.__or__, |
714 | 715 |
(models.Q(remote_items__contains=remote_item_id) for remote_item_id in remote_item_ids), |
715 | 716 |
) |
716 | 717 | |
717 | 718 |
# accumulate in paid_items each remote_item earliest payment_date |
718 |
for transaction in transaction_qs.filter(query): |
|
719 |
for transaction in queryset.filter(query): |
|
720 |
for remote_item_id in transaction.remote_items.split(','): |
|
721 |
if remote_item_id in remote_item_ids: |
|
722 |
yield transaction |
|
723 |
break |
|
724 | ||
725 |
@classmethod |
|
726 |
def update_paid(cls, regie, remote_items): |
|
727 |
paid_items = {} |
|
728 |
waiting_items = {} |
|
729 |
transaction_qs = Transaction.objects.filter(regie=regie) |
|
730 | ||
731 |
can_poll_backend = regie.can_poll_backend() |
|
732 | ||
733 |
# accumulate in paid_items each remote_item earliest payment_date |
|
734 |
for transaction in cls.transactions_for_remote_items(transaction_qs, remote_items): |
|
735 |
if transaction.is_running() and can_poll_backend: |
|
736 |
transaction.poll_backend() |
|
719 | 737 |
for remote_item in transaction.remote_items.split(','): |
720 |
if remote_item not in paid_items: |
|
721 |
paid_items[remote_item] = transaction.end_date |
|
722 |
else: |
|
723 |
paid_items[remote_item] = min(transaction.end_date, paid_items[remote_item]) |
|
738 |
if transaction.end_date and transaction.is_paid(): |
|
739 |
if remote_item not in paid_items: |
|
740 |
paid_items[remote_item] = transaction.end_date |
|
741 |
else: |
|
742 |
paid_items[remote_item] = min(transaction.end_date, paid_items[remote_item]) |
|
743 |
elif transaction.status == eopayment.WAITING and can_poll_backend: |
|
744 |
waiting_items[remote_item] = transaction.start_date |
|
724 | 745 | |
725 | 746 |
# update remote_item.paid using paid_items |
726 | 747 |
for remote_item in remote_items: |
... | ... | |
729 | 750 |
if remote_item.id in paid_items: |
730 | 751 |
remote_item.paid = True |
731 | 752 |
remote_item.payment_date = paid_items[remote_item.id] |
753 |
elif remote_item.id in waiting_items: |
|
754 |
remote_item.waiting_date = waiting_items[remote_item.id] |
|
732 | 755 | |
733 | 756 | |
734 | 757 |
def status_label(status): |
... | ... | |
758 | 781 |
amount = models.DecimalField(default=0, max_digits=7, decimal_places=2) |
759 | 782 | |
760 | 783 |
RUNNING_STATUSES = [0, eopayment.WAITING, eopayment.RECEIVED] |
784 |
PAID_STATUSES = [eopayment.PAID, eopayment.ACCEPTED] |
|
761 | 785 | |
762 | 786 |
def is_remote(self): |
763 | 787 |
return self.remote_items != '' |
... | ... | |
768 | 792 |
return _('Anonymous User') |
769 | 793 | |
770 | 794 |
def is_paid(self): |
771 |
return self.status in (eopayment.PAID, eopayment.ACCEPTED)
|
|
795 |
return self.status in self.PAID_STATUSES
|
|
772 | 796 | |
773 | 797 |
def is_running(self): |
774 | 798 |
return self.status in self.RUNNING_STATUSES |
... | ... | |
1012 | 1036 |
# list transactions : |
1013 | 1037 |
# * paid by the user |
1014 | 1038 |
# * or linked to a BasketItem of the user |
1015 |
return Transaction.objects.filter(models.Q(user=user) | models.Q(items__user=user)).filter(
|
|
1039 |
qs = Transaction.objects.filter(models.Q(user=user) | models.Q(items__user=user)).filter(
|
|
1016 | 1040 |
start_date__gte=timezone.now() - datetime.timedelta(days=7) |
1017 | 1041 |
) |
1042 |
for transaction in qs: |
|
1043 |
if transaction.can_poll_backend() and transaction.is_running(): |
|
1044 |
transaction.poll_backend() |
|
1045 |
return qs |
|
1018 | 1046 | |
1019 | 1047 |
def is_relevant(self, context): |
1020 | 1048 |
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated): |
combo/apps/lingo/templates/lingo/combo/item.html | ||
---|---|---|
48 | 48 |
{% if item.no_online_payment_reason_details %} |
49 | 49 |
<div class="no-online-payment-reason"><span>{{ item.no_online_payment_reason_details }}</span></div> |
50 | 50 |
{% endif %} |
51 |
{% if not item.paid and item.online_payment and item.amount >= regie.payment_min_amount %} |
|
51 |
{% if item.waiting_date and not item.paid %} |
|
52 |
<div class="paid paid-info">{% trans "Waiting for payment." %}</div> |
|
53 |
{% endif %} |
|
54 |
{% if not item.paid and item.online_payment and item.amount >= regie.payment_min_amount and not item.waiting_date %} |
|
52 | 55 |
{% csrf_token %} |
53 | 56 |
{% if not user.is_authenticated %} |
54 | 57 |
<div class="email"> |
combo/apps/lingo/templates/lingo/combo/items.html | ||
---|---|---|
51 | 51 |
{% if item.regie.is_remote %} |
52 | 52 |
<td> |
53 | 53 |
<a href="{% url 'view-item' regie_id=item.regie.pk item_crypto_id=item.crypto_id %}?page={{ cell.page.pk }}" rel="popup" class="icon-view">{% trans "View" %} |
54 |
{% if item.online_payment and item.amount >= item.regie.payment_min_amount %}{% trans "and pay" %}{% endif %}
|
|
54 |
{% if item.online_payment and item.amount >= item.regie.payment_min_amount and not item.waiting_date %}{% trans "and pay" %}{% endif %}
|
|
55 | 55 |
</a> |
56 | 56 |
{% if item.has_pdf %} |
57 | 57 |
<br/><a href="{% url 'download-item-pdf' regie_id=item.regie.pk item_crypto_id=item.crypto_id %}" class="icon-pdf" |
combo/apps/lingo/views.py | ||
---|---|---|
58 | 58 |
PaymentException, |
59 | 59 |
Regie, |
60 | 60 |
RemoteInvoiceException, |
61 |
RemoteItem, |
|
61 | 62 |
SelfDeclaredInvoicePayment, |
62 | 63 |
Transaction, |
63 | 64 |
TransactionOperation, |
... | ... | |
389 | 390 |
if bool(len(items)) == bool(len(remote_items)): |
390 | 391 |
messages.error(request, _('Items to pay are missing or are not of the same type (local/remote).')) |
391 | 392 |
return HttpResponseRedirect(next_url) |
393 | ||
394 |
if ( |
|
395 |
regie.payment_backend.can_poll_backend() |
|
396 |
and self.poll_for_newly_paid_or_still_running_transactions(regie, items, remote_items) |
|
397 |
): |
|
398 |
messages.error(request, _('Some items are already paid or are being paid.')) |
|
399 |
return HttpResponseRedirect(next_url) |
|
400 | ||
392 | 401 |
if regie.can_pay_only_one_basket_item and (len(items) > 1 or len(remote_items) > 1): |
393 | 402 |
messages.error(request, _('This regie allows to pay only one item.')) |
394 | 403 |
return HttpResponseRedirect(next_url) |
... | ... | |
491 | 500 | |
492 | 501 |
raise NotImplementedError() |
493 | 502 | |
503 |
def poll_for_newly_paid_or_still_running_transactions(self, regie, items, remote_items): |
|
504 |
'''Verify if any open transaction is not already paid.''' |
|
505 |
qs = Transaction.objects.filter(regie=regie, status__in=Transaction.RUNNING_STATUSES) |
|
506 |
if items: |
|
507 |
transactions = qs.filter(items__in=items) |
|
508 |
else: |
|
509 |
transactions = RemoteItem.transactions_for_remote_items(qs, remote_items) |
|
510 | ||
511 |
newly_paid_or_still_running = False |
|
512 |
for transaction in transactions: |
|
513 |
transaction.poll_backend() |
|
514 |
newly_paid_or_still_running |= transaction.is_paid() or transaction.is_running() |
|
515 |
return newly_paid_or_still_running |
|
516 | ||
494 | 517 | |
495 | 518 |
class PayView(PayMixin, View): |
496 | 519 |
def post(self, request, *args, **kwargs): |
combo/public/views.py | ||
---|---|---|
586 | 586 |
} |
587 | 587 |
ctx.update(getattr(request, 'extra_context_data', {})) |
588 | 588 |
modify_global_context(request, ctx) |
589 |
if getattr(settings, 'COMBO_TEST_SYNCHRONOUS', False): |
|
590 |
ctx['synchronous'] = True |
|
589 | 591 | |
590 | 592 |
for cell in cells: |
591 | 593 |
if cell.modify_global_context: |
tests/test_lingo_payment.py | ||
---|---|---|
26 | 26 |
EXPIRED, |
27 | 27 |
BasketItem, |
28 | 28 |
LingoBasketCell, |
29 |
LingoRecentTransactionsCell, |
|
29 | 30 |
PaymentBackend, |
30 | 31 |
Regie, |
31 | 32 |
Transaction, |
... | ... | |
2090 | 2091 |
) as transaction_poll_backend: |
2091 | 2092 |
payment_backend.poll_backend() |
2092 | 2093 |
transaction_poll_backend.assert_called_once_with(transaction) |
2094 | ||
2095 |
@mock.patch('eopayment.payfip_ws.Payment.payment_status') |
|
2096 |
@mock.patch('eopayment.payfip_ws.Payment.request', return_value=(1, eopayment.URL, 'https://payfip/')) |
|
2097 |
def test_payfip_poll_for_newly_paid_transactions( |
|
2098 |
self, payment_request, payment_status, app, basket_page, mono_regie, user |
|
2099 |
): |
|
2100 |
item = BasketItem.objects.create( |
|
2101 |
user=user, |
|
2102 |
regie=mono_regie, |
|
2103 |
amount=42, |
|
2104 |
subject='foo item', |
|
2105 |
request_data={'refdet': 'F20201030', 'exer': '2020'}, |
|
2106 |
) |
|
2107 |
cell = LingoRecentTransactionsCell(page=basket_page, placeholder='content', order=1) |
|
2108 |
cell.save() |
|
2109 | ||
2110 |
login(app) |
|
2111 | ||
2112 |
# Try to pay |
|
2113 |
pay_resp = app.get('/test_basket_cell/') |
|
2114 |
assert 'foo item' in pay_resp |
|
2115 |
assert 'Running' not in pay_resp |
|
2116 |
resp = pay_resp.click('Pay') |
|
2117 |
# we are redirect to payfip |
|
2118 |
assert resp.location == 'https://payfip/' |
|
2119 | ||
2120 |
transaction = Transaction.objects.filter(items__in=[item]).get() |
|
2121 | ||
2122 |
# Simulate still running status on polling |
|
2123 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
2124 |
signed=True, |
|
2125 |
result=eopayment.WAITING, |
|
2126 |
order_id=transaction.order_id, |
|
2127 |
) |
|
2128 | ||
2129 |
# Try to pay again |
|
2130 |
resp = app.get('/test_basket_cell/') |
|
2131 |
assert 'foo item' not in resp |
|
2132 |
assert 'Pay' not in resp |
|
2133 |
assert 'Running' in resp |
|
2134 |
resp = pay_resp.click('Pay').follow() |
|
2135 |
assert 'Some items are already paid or' in resp |
|
2136 |
assert 'foo item' not in resp |
|
2137 |
assert 'Running' in resp |
|
2138 | ||
2139 |
# Simulate still paid status on polling |
|
2140 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
2141 |
signed=True, |
|
2142 |
result=eopayment.PAID, |
|
2143 |
order_id=transaction.order_id, |
|
2144 |
) |
|
2145 | ||
2146 |
# Try to pay again |
|
2147 |
resp = app.get('/test_basket_cell/') |
|
2148 |
assert 'foo item: 42.00' in resp |
|
2149 |
assert 'Pay' not in resp |
|
2150 |
assert 'Running' not in resp |
tests/test_lingo_remote_regie.py | ||
---|---|---|
5 | 5 |
from decimal import Decimal |
6 | 6 | |
7 | 7 |
import eopayment |
8 |
import httmock |
|
8 | 9 |
import mock |
9 | 10 |
import pytest |
10 | 11 |
from django.apps import apps |
... | ... | |
32 | 33 |
from combo.data.models import Page |
33 | 34 |
from combo.utils import aes_hex_encrypt, check_query |
34 | 35 | |
36 |
from .test_manager import login |
|
37 | ||
35 | 38 |
pytestmark = pytest.mark.django_db |
36 | 39 | |
37 | 40 | |
... | ... | |
742 | 745 |
assert 'http://localhost' in html_message |
743 | 746 |
assert mailoutbox[0].attachments[0][0] == '01.pdf' |
744 | 747 |
assert mailoutbox[0].attachments[0][2] == 'application/pdf' |
748 | ||
749 | ||
750 |
@pytest.fixture |
|
751 |
def remote_invoices_httmock(): |
|
752 |
invoices = [] |
|
753 |
invoice = {} |
|
754 | ||
755 |
netloc = 'remote.regie.example.com' |
|
756 | ||
757 |
@httmock.urlmatch(netloc=netloc, path='^/invoice/') |
|
758 |
def invoice_mock(url, request): |
|
759 |
return json.dumps({'err': 0, 'data': invoice}) |
|
760 | ||
761 |
@httmock.urlmatch(netloc=netloc, path='^/invoices/') |
|
762 |
def invoices_mock(url, request): |
|
763 |
return json.dumps({'err': 0, 'data': invoices}) |
|
764 | ||
765 |
context_manager = httmock.HTTMock(invoices_mock, invoice_mock) |
|
766 |
context_manager.url = 'https://%s/' % netloc |
|
767 |
context_manager.invoices = invoices |
|
768 |
context_manager.invoice = invoice |
|
769 |
with context_manager: |
|
770 |
yield context_manager |
|
771 | ||
772 | ||
773 |
@mock.patch('eopayment.payfip_ws.Payment.payment_status') |
|
774 |
@mock.patch('eopayment.payfip_ws.Payment.request', return_value=(1, eopayment.URL, 'https://payfip/')) |
|
775 |
def test_remote_regie_poll_backend( |
|
776 |
payment_request, payment_status, app, remote_regie, settings, remote_invoices_httmock |
|
777 |
): |
|
778 |
settings.COMBO_TEST_SYNCHRONOUS = True |
|
779 | ||
780 |
remote_invoices_httmock.invoices.extend(INVOICES) |
|
781 |
remote_invoices_httmock.invoice.update(INVOICES[0]) |
|
782 |
remote_regie.webservice_url = remote_invoices_httmock.url |
|
783 |
remote_regie.save() |
|
784 |
# use payfip |
|
785 |
remote_regie.payment_backend.service = 'payfip_ws' |
|
786 |
remote_regie.payment_backend.save() |
|
787 | ||
788 |
User.objects.create_user('admin', password='admin', email='foo@example.com') |
|
789 |
page = Page.objects.create(title='xxx', slug='test_basket_cell', template_name='standard') |
|
790 |
ActiveItems.objects.create(regie='remote', page=page, placeholder='content', order=0) |
|
791 | ||
792 |
login(app) |
|
793 | ||
794 |
assert Transaction.objects.count() == 0 |
|
795 | ||
796 |
resp = app.get('/test_basket_cell/?sync') |
|
797 |
assert 'F-2016-One' in resp |
|
798 | ||
799 |
resp = resp.click('pay', index=0) |
|
800 |
pay_resp = resp |
|
801 |
resp = resp.form.submit('Pay') |
|
802 | ||
803 |
transaction = Transaction.objects.get() |
|
804 |
assert transaction.status == 0 |
|
805 | ||
806 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
807 |
signed=True, |
|
808 |
result=eopayment.WAITING, |
|
809 |
order_id=transaction.order_id, |
|
810 |
) |
|
811 | ||
812 |
assert payment_status.call_count == 0 |
|
813 |
resp = app.get('/test_basket_cell/?sync') |
|
814 |
assert 'F-2016-One' in resp |
|
815 |
assert payment_status.call_count == 1 |
|
816 |
transaction.refresh_from_db() |
|
817 |
assert transaction.status == eopayment.WAITING |
|
818 | ||
819 |
resp = resp.click('pay', index=0) |
|
820 |
assert 'Waiting for payment' in resp |
|
821 |
assert 'button' not in resp |
|
822 | ||
823 |
resp = pay_resp.form.submit('Pay').follow() |
|
824 |
assert 'Some items are already paid' in resp |
|
825 | ||
826 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
827 |
signed=True, |
|
828 |
result=eopayment.PAID, |
|
829 |
order_id=transaction.order_id, |
|
830 |
) |
|
831 | ||
832 |
resp = app.get('/test_basket_cell/?sync') |
|
833 |
assert 'F-2016-One' not in resp |
|
834 |
transaction.refresh_from_db() |
|
835 |
assert transaction.status == eopayment.PAID |
|
745 |
- |