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