diff --git a/combo/apps/lingo/management/commands/notify_new_remote_invoices.py b/combo/apps/lingo/management/commands/notify_new_remote_invoices.py
new file mode 100644
index 00000000..bdad8032
--- /dev/null
+++ b/combo/apps/lingo/management/commands/notify_new_remote_invoices.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+#
+# lingo - basket and payment system
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import logging
+
+from django.core.management.base import BaseCommand
+
+from combo.apps.lingo.models import Regie
+
+
+class Command(BaseCommand):
+
+ def handle(self, *args, **kwargs):
+ logger = logging.getLogger(__name__)
+ for regie in Regie.objects.exclude(webservice_url=''):
+ try:
+ regie.notify_new_remote_invoices()
+ except Exception, e:
+ logger.exception('error while notifying new remote invoices: %s', e)
diff --git a/combo/apps/lingo/models.py b/combo/apps/lingo/models.py
index 3c9efa67..bbd26083 100644
--- a/combo/apps/lingo/models.py
+++ b/combo/apps/lingo/models.py
@@ -32,14 +32,17 @@ from django.conf import settings
from django.db import models
from django.forms import models as model_forms, Select
from django.utils.translation import ugettext_lazy as _
-from django.utils import timezone
+from django.utils import timezone, dateparse
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.utils.http import urlencode
+from django.contrib.auth.models import User
+
from combo.data.fields import RichTextField
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from combo.utils import NothingInCacheException, aes_hex_encrypt, requests
+from combo.apps.notifications.models import Notification
EXPIRED = 9999
@@ -203,6 +206,71 @@ class Regie(models.Model):
extra_fee=True,
user_cancellable=False).save()
+ def get_remote_pending_invoices(self):
+ if not self.is_remote():
+ return {}
+ url = self.webservice_url + '/users/with-pending-invoices/'
+ response = requests.get(url, remote_service='auto', cache_duration=0,
+ log_errors=False)
+ if not response.ok:
+ return {}
+ return response.json()['data']
+
+ def get_notification_namespace(self):
+ return 'invoice-%s' % self.slug
+
+ def get_notification_id(self, invoice):
+ return '%s:%s' % (self.get_notification_namespace(), invoice['id'])
+
+ def get_notification_reminder_id(self, invoice):
+ return '%s:reminder-%s' % (self.get_notification_namespace(), invoice['id'])
+
+ def notify_invoice(self, user, invoice):
+ now = timezone.now()
+ remind_delta = timezone.timedelta(days=settings.LINGO_NEW_INVOICES_REMIND_DELTA)
+ pay_limit_date = timezone.make_aware(dateparse.parse_datetime(invoice['pay_limit_date']))
+ active_items_cell = ActiveItems.objects.first()
+ if active_items_cell:
+ items_page_url = active_items_cell.page.get_online_url()
+ else:
+ items_page_url = ''
+ notification_id = self.get_notification_id(invoice)
+ notification_reminder_id = self.get_notification_reminder_id(invoice)
+ if pay_limit_date < now:
+ # invoice is out of date
+ Notification.forget(user, notification_id)
+ Notification.forget(user, notification_reminder_id)
+ else:
+ # invoice can be paid
+ if pay_limit_date > now + remind_delta:
+ message = _('Invoice %s to pay') % invoice['label']
+ else:
+ message = _('Reminder: invoice %s to pay') % invoice['label']
+ notification_id = notification_reminder_id
+ Notification.notify(user,
+ summary=message,
+ id=notification_id,
+ url=items_page_url,
+ end_timestamp=pay_limit_date)
+ return notification_id
+
+ def notify_new_remote_invoices(self):
+ pending_invoices = self.get_remote_pending_invoices()
+ notification_ids = []
+ for uuid, items in pending_invoices.iteritems():
+ try:
+ user = User.objects.get(username=uuid)
+ except User.DoesNotExist:
+ continue
+ for invoice in items['invoices']:
+ if Decimal(invoice['total_amount']) >= self.payment_min_amount:
+ notification_ids.append(
+ self.notify_invoice(user, invoice))
+ # clear old notifications for invoice not in the source anymore
+ Notification.objects.namespace(self.get_notification_namespace())\
+ .exclude(external_id__in=notification_ids) \
+ .forget()
+
class BasketItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
diff --git a/combo/settings.py b/combo/settings.py
index f4421886..4460d016 100644
--- a/combo/settings.py
+++ b/combo/settings.py
@@ -285,6 +285,9 @@ COMBO_MAP_TILE_URLTEMPLATE = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
# default combo map attribution
COMBO_MAP_ATTRIBUTION = 'Map data © OpenStreetMap contributors, CC-BY-SA'
+# default delta, in days, for invoice remind notifications
+LINGO_NEW_INVOICES_REMIND_DELTA = 7
+
# timeout used in python-requests call, in seconds
# we use 28s by default: timeout just before web server, which is usually 30s
REQUESTS_TIMEOUT = 28
diff --git a/tests/test_notification.py b/tests/test_notification.py
index 671b2706..26c9ec96 100644
--- a/tests/test_notification.py
+++ b/tests/test_notification.py
@@ -1,6 +1,8 @@
import json
+import mock
import pytest
+from decimal import Decimal
from django.contrib.auth.models import User
from django.test.client import RequestFactory
@@ -11,6 +13,7 @@ from django.test import Client
from combo.data.models import Page
from combo.apps.notifications.models import Notification, NotificationsCell
+from combo.apps.lingo.models import Regie, ActiveItems
pytestmark = pytest.mark.django_db
@@ -42,6 +45,21 @@ def login(username='admin', password='admin'):
assert resp.status_code == 302
+@pytest.fixture
+def regie():
+ try:
+ regie = Regie.objects.get(slug='remote')
+ except Regie.DoesNotExist:
+ regie = Regie()
+ regie.label = 'Remote'
+ regie.slug = 'remote'
+ regie.description = 'remote'
+ regie.payment_min_amount = Decimal(2.0)
+ regie.service = 'dummy'
+ regie.save()
+ return regie
+
+
def test_notification_api(user, user2):
notification = Notification.notify(user, 'notifoo')
assert Notification.objects.count() == 1
@@ -255,3 +273,105 @@ def test_notification_id_and_origin(user):
result = notify({'summary': 'foo', 'id': 'foo:foo', 'origin': 'bar'})
assert result['err'] == 0
+
+
+@mock.patch('combo.apps.lingo.models.requests.get')
+def test_notify_remote_items(mock_get, app, user, user2, regie):
+
+ datetime_format = '%Y-%m-%dT%H:%M:%S'
+ invoice_now = now()
+ creation_date = (invoice_now - timedelta(days=1)).strftime(datetime_format)
+ pay_limit_date = (invoice_now + timedelta(days=20)).strftime(datetime_format)
+ new_pay_limit_date = (invoice_now + timedelta(days=5)).strftime(datetime_format)
+ FAKE_PENDING_INVOICES = {
+ "data":
+ {
+ "admin": {
+ "invoices": [
+ {
+ 'id': '01',
+ 'label': '010101',
+ 'total_amount': '10',
+ 'amount': '10',
+ 'created': creation_date,
+ 'pay_limit_date': pay_limit_date,
+ 'has_pdf': False,
+ },
+ {
+ 'id': '011',
+ 'label': '0101011',
+ 'total_amount': '1.5',
+ 'amount': '1.5',
+ 'created': creation_date,
+ 'pay_limit_date': pay_limit_date,
+ 'has_pdf': False,
+ }
+ ]
+ },
+ 'admin2': {
+ 'invoices': [
+ {
+ 'id': '02',
+ 'label': '020202',
+ 'total_amount': '2.0',
+ 'amount': '2.0',
+ 'created': creation_date,
+ 'pay_limit_date': pay_limit_date,
+ 'has_pdf': False,
+ }
+ ]
+ },
+ 'foo': {
+ 'invoices': [
+ {
+
+ 'id': 'O3',
+ 'label': '030303',
+ 'total_amount': '42',
+ 'amount': '42',
+ 'created': creation_date,
+ 'pay_limit_date': pay_limit_date,
+ 'has_pdf': False,
+ }
+ ]
+ }
+ }
+ }
+ mock_response = mock.Mock(status_code=200, content=json.dumps(FAKE_PENDING_INVOICES))
+ mock_response.json.return_value = FAKE_PENDING_INVOICES
+ mock_get.return_value = mock_response
+ regie.notify_new_remote_invoices()
+ assert mock_get.call_count == 0
+ regie.webservice_url = 'http://example.org/regie' # is_remote
+ regie.save()
+ regie.notify_new_remote_invoices()
+
+ assert Notification.objects.filter(external_id__startswith='invoice-%s:' % regie.slug).visible().new().count() == 2
+ assert Notification.objects.filter(external_id__startswith='invoice-%s:reminder-' % regie.slug).count() == 0
+ assert Notification.objects.count() == 2
+ for notif in Notification.objects.all():
+ assert notif.url == '', notif.id
+
+ page = Page(title='Active Items', slug='active_items', template_name='standard')
+ page.save()
+ cell = ActiveItems(page=page, placeholder='content', order=0)
+ cell.save()
+
+ for user in FAKE_PENDING_INVOICES['data']:
+ for invoice in FAKE_PENDING_INVOICES['data'][user]['invoices']:
+ invoice['pay_limit_date'] = new_pay_limit_date
+
+ # create remind notifications
+ regie.notify_new_remote_invoices()
+ assert Notification.objects.exclude(external_id__startswith='invoice-%s:reminder-' % regie.slug) \
+ .visible().count() == 0
+ assert Notification.objects.filter(external_id__startswith='invoice-%s:reminder-' % regie.slug) \
+ .visible().new().count() == 2
+ assert Notification.objects.count() == 4
+
+ # url appeared on new new reminder notifications
+ assert len([notif for notif in Notification.objects.all() if notif.url == page.get_online_url()]) == 2
+
+ # be sure the are no more reminders created
+ regie.notify_new_remote_invoices()
+ assert Notification.objects.count() == 4