From c8ba7c191234291726ca9bda05da5f0c05c1cf9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 29 Nov 2019 15:35:09 +0100
Subject: [PATCH 1/3] lingo: new page to display payments in error (#21626)

---
 combo/apps/lingo/manager_views.py             |  33 +++-
 .../lingo/basketitem_error_list.html          |  50 ++++++
 .../templates/lingo/transaction_list.html     |   1 +
 combo/apps/lingo/urls.py                      |   8 +-
 tests/test_lingo_manager.py                   | 166 ++++++++++++++++++
 5 files changed, 253 insertions(+), 5 deletions(-)
 create mode 100644 combo/apps/lingo/templates/lingo/basketitem_error_list.html

diff --git a/combo/apps/lingo/manager_views.py b/combo/apps/lingo/manager_views.py
index 0dc5fd3..1c26470 100644
--- a/combo/apps/lingo/manager_views.py
+++ b/combo/apps/lingo/manager_views.py
@@ -20,8 +20,9 @@ from dateutil import parser as date_parser
 
 from django.core.urlresolvers import reverse_lazy
 from django.db.models import Q
+from django.db.models.expressions import RawSQL
 from django.utils import six
-from django.utils.timezone import make_aware
+from django.utils.timezone import make_aware, now
 from django.views.generic import CreateView, UpdateView, ListView, DeleteView
 from django.http import HttpResponse
 from django.template.response import TemplateResponse
@@ -30,7 +31,7 @@ import eopayment
 
 from .forms import RegieForm
 from .forms import TransactionExportForm
-from .models import PaymentBackend, Regie, Transaction
+from .models import BasketItem, PaymentBackend, Regie, Transaction
 
 
 class RegieListView(ListView):
@@ -103,6 +104,34 @@ class TransactionListView(ListView):
         return qs
 
 
+class BasketItemErrorListView(ListView):
+    model = BasketItem
+    template_name = 'lingo/basketitem_error_list.html'
+    paginate_by = 10
+
+    def get_queryset(self):
+        raw = (
+            'SELECT %s FROM lingo_transaction '
+            'INNER JOIN lingo_transaction_items '
+            'ON "lingo_transaction"."id" = "lingo_transaction_items"."transaction_id" '
+            'AND "lingo_transaction_items"."basketitem_id"="lingo_basketitem"."id" '
+            'ORDER BY start_date DESC LIMIT 1')
+        queryset = (
+            BasketItem.objects
+            .annotate(bank_transaction_id=RawSQL(raw % 'bank_transaction_id', []))
+            .annotate(transaction_status=RawSQL(raw % 'status', []))
+            .filter(regie__webservice_url='')
+            .order_by())
+        queryset1 = (
+            queryset
+            .filter(
+                notification_date__isnull=True,
+                cancellation_date__isnull=True,
+                payment_date__lt=now() - datetime.timedelta(minutes=300)))
+        queryset2 = queryset.filter(transaction_status=eopayment.ERROR)
+        return queryset1.union(queryset2).order_by('-creation_date')
+
+
 def download_transactions_csv(request):
     if request.method == 'POST':
         form = TransactionExportForm(data=request.POST)
diff --git a/combo/apps/lingo/templates/lingo/basketitem_error_list.html b/combo/apps/lingo/templates/lingo/basketitem_error_list.html
new file mode 100644
index 0000000..a689e37
--- /dev/null
+++ b/combo/apps/lingo/templates/lingo/basketitem_error_list.html
@@ -0,0 +1,50 @@
+{% extends "lingo/manager_base.html" %}
+{% load i18n %}
+
+{% block appbar %}
+<h2>{% trans 'Payments in error' %}</h2>
+{% endblock %}
+
+{% block breadcrumb %}
+{{ block.super }}
+<a href="{% url 'lingo-manager-payment-error-list' %}">{% trans 'Payments in error' %}</a>
+{% endblock %}
+
+{% block content %}
+
+{% if object_list %}
+<table class="main">
+<thead>
+  <tr>
+   <th>{% trans 'Item' %}</th>
+   <th>{% trans 'Amount' %}</th>
+   <th colspan="2">{% trans 'Date' %}</th>
+  </tr>
+</thead>
+<tbody>
+{% for object in object_list %}
+<tr>
+  <td>{% if object.source_url %}<a href="{{ object.source_url }}">{{ object.subject }}</a>{% else %}{{ object.subject }}{% endif %}</td>
+  <td class="price">{{ object.amount }} €</td>
+  <td>{{ object.payment_date }}</td>
+  <td>
+    {% if object.transaction_status != 99 %}
+      <a href="{% url 'lingo-manager-homepage' %}?q={{ object.bank_transaction_id }}">{% trans "See transaction" %}</a>
+    {% endif %}
+  </td>
+</tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% include "gadjo/pagination.html" %}
+
+{% else %}
+<div class="big-msg-info">
+  {% blocktrans %}
+  This site doesn't have any payment in error.
+  {% endblocktrans %}
+</div>
+{% endif %}
+
+{% endblock %}
diff --git a/combo/apps/lingo/templates/lingo/transaction_list.html b/combo/apps/lingo/templates/lingo/transaction_list.html
index 2530030..6dd650f 100644
--- a/combo/apps/lingo/templates/lingo/transaction_list.html
+++ b/combo/apps/lingo/templates/lingo/transaction_list.html
@@ -4,6 +4,7 @@
 {% block appbar %}
 <h2>{% trans 'Transactions' %}</h2>
 <span class="actions">
+<a href="{% url 'lingo-manager-payment-error-list' %}">{% trans 'Payments in error' %}</a>
 <a href="{% url 'lingo-manager-paymentbackend-list' %}">{% trans 'Payment backends' %}</a>
 <a href="{% url 'lingo-manager-regie-list' %}">{% trans 'Regies' %}</a>
 <a rel="popup" href="{% url 'lingo-manager-transactions-download' %}" data-autoclose-dialog="true">{% trans 'download CSV' %}</a>
diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py
index 5498d9f..a0d0a61 100644
--- a/combo/apps/lingo/urls.py
+++ b/combo/apps/lingo/urls.py
@@ -23,12 +23,14 @@ from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
                     RemoveBasketItemApiView, ValidateTransactionApiView,
                     CancelTransactionApiView, SelfInvoiceView, BasketItemPayView)
 from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
-                            RegieDeleteView, TransactionListView, download_transactions_csv,
-                            PaymentBackendListView, PaymentBackendCreateView,
-                            PaymentBackendUpdateView, PaymentBackendDeleteView)
+                            RegieDeleteView, TransactionListView, BasketItemErrorListView,
+                            download_transactions_csv, PaymentBackendListView,
+                            PaymentBackendCreateView, PaymentBackendUpdateView,
+                            PaymentBackendDeleteView)
 
 lingo_manager_urls = [
     url('^$', TransactionListView.as_view(), name='lingo-manager-homepage'),
+    url('^payments/error/$', BasketItemErrorListView.as_view(), name='lingo-manager-payment-error-list'),
     url('^transactions/download-csv/$', download_transactions_csv, name='lingo-manager-transactions-download'),
     url('^regies/$', RegieListView.as_view(), name='lingo-manager-regie-list'),
     url('^regies/add/$', RegieCreateView.as_view(), name='lingo-manager-regie-add'),
diff --git a/tests/test_lingo_manager.py b/tests/test_lingo_manager.py
index 3421242..f1f4c42 100644
--- a/tests/test_lingo_manager.py
+++ b/tests/test_lingo_manager.py
@@ -227,6 +227,172 @@ def test_transactions_search(app, admin_user):
     assert resp.text.count('<tr') == 0
     assert 'No transactions found matching' in resp.text
 
+
+def test_basketitem_error_list(app, admin_user, payment_backend):
+    regie = Regie.objects.create(
+        label='test-regie',
+        slug='test-regie',
+        payment_backend=payment_backend)
+    user = User.objects.create_user('dimebag', 'dime@bag.pan', 'pwd')
+    regie_with_webservice_url = Regie.objects.create(
+        label='test-regie2',
+        slug='test-regie2',
+        payment_backend=payment_backend,
+        webservice_url='http://example.net')
+
+    date_now = now()
+    date_in_past = date_now - datetime.timedelta(minutes=300)
+
+    # item with payment_date and notification_date, status PAID
+    # => not displayed
+    item1 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 1',
+        source_url='http://example.net/1',
+        amount=1,
+        payment_date=date_in_past,
+        notification_date=date_in_past)
+    transaction11 = Transaction.objects.create(
+        status=eopayment.ERROR,
+        order_id='order id 1.1',
+        bank_transaction_id='bank_id_11',
+        amount=1)
+    transaction11.items.add(item1)
+    transaction12 = Transaction.objects.create(
+        status=eopayment.PAID,
+        order_id='order id 1.2',
+        bank_transaction_id='bank_id_12',
+        amount=1)
+    transaction12.items.add(item1)
+
+    # item with payment_date and notification_date, status ERROR
+    # => displayed
+    item2 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 2',
+        source_url='',
+        amount=2,
+        payment_date=date_in_past,
+        notification_date=date_in_past)
+    transaction21 = Transaction.objects.create(
+        status=eopayment.ERROR,
+        order_id='order id 2.1',
+        bank_transaction_id='bank_id_21',
+        amount=2)
+    transaction21.items.add(item2)
+    transaction22 = Transaction.objects.create(
+        status=eopayment.ERROR,
+        order_id='order id 2.2',
+        bank_transaction_id='bank_id_22',
+        amount=2)
+    transaction22.items.add(item2)
+    # the same but for the other regie
+    item2_bis = BasketItem.objects.create(
+        user=user,
+        regie=regie_with_webservice_url,
+        subject='item 2 bis',
+        source_url='',
+        amount=22222,
+        payment_date=date_in_past,
+        notification_date=date_in_past)
+    transaction2_bis = Transaction.objects.create(
+        status=eopayment.ERROR,
+        order_id='order id 2 bis',
+        bank_transaction_id='bank_id_2_bis',
+        amount=22222)
+    transaction2_bis.items.add(item2_bis)
+
+    # item without dates, status ERROR
+    # => displayed
+    item3 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 3',
+        source_url='http://example.net/3',
+        amount=3)
+    transaction3 = Transaction.objects.create(
+        status=eopayment.ERROR,
+        order_id='order id 3',
+        bank_transaction_id='bank_id_3',
+        amount=3)
+    transaction3.items.add(item3)
+
+    # item with payment_date but no notification_date, too young
+    # => not displayed
+    item4 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 4',
+        source_url='http://example.net/4',
+        amount=4,
+        payment_date=date_now,
+        notification_date=None)
+    transaction4 = Transaction.objects.create(
+        order_id='order id 4',
+        bank_transaction_id='bank_id_4',
+        amount=4)
+    transaction4.items.add(item4)
+
+    # item with payment_date but no notification_date, in the past
+    # => displayed
+    item5 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 5',
+        source_url='http://example.net/5',
+        amount=5,
+        payment_date=date_in_past,
+        notification_date=None)
+    transaction5 = Transaction.objects.create(
+        order_id='order id 5',
+        bank_transaction_id='bank_id_5',
+        amount=5)
+    transaction5.items.add(item5)
+    # the same but for the other regie
+    item5_bis = BasketItem.objects.create(
+        user=user,
+        regie=regie_with_webservice_url,
+        subject='item 5 bis',
+        source_url='http://example.net/5_bis',
+        amount=55555,
+        payment_date=date_in_past,
+        notification_date=None)
+    transaction5_bis = Transaction.objects.create(
+        order_id='order id 5 bis',
+        bank_transaction_id='bank_id_5_bis',
+        amount=55555)
+    transaction5_bis.items.add(item5_bis)
+
+    # item with payment_date, no notification_date, but a cancellation_date, in the past
+    # => not displayed
+    item6 = BasketItem.objects.create(
+        user=user,
+        regie=regie,
+        subject='item 6',
+        source_url='http://example.net/6',
+        amount=6,
+        payment_date=date_in_past,
+        notification_date=None,
+        cancellation_date=date_now)
+    transaction6 = Transaction.objects.create(
+        order_id='order id 6',
+        bank_transaction_id='bank_id_6',
+        amount=6)
+    transaction6.items.add(item6)
+
+    app = login(app)
+    resp = app.get('/manage/lingo/payments/error/', status=200)
+    assert list(resp.context['object_list']) == [item5, item3, item2]
+    assert '<a href="%s">' % item5.source_url in resp.text
+    assert '<a href="%s">' % item3.source_url in resp.text
+    assert '<a href="%s">' % item2.source_url not in resp.text
+    assert '/manage/lingo/?q=%s' % transaction5.bank_transaction_id in resp.text
+    assert '/manage/lingo/?q=%s' % transaction3.bank_transaction_id not in resp.text
+    assert '/manage/lingo/?q=%s' % transaction22.bank_transaction_id not in resp.text
+
+
 def test_configure_tipi_cell(app, admin_user):
     page = Page(title='tipi', slug='tipi', template_name='standard')
     page.save()
-- 
2.20.1

