lingo: notify new remote invoices (#13122)

This commit is contained in:
Serghei Mihai 2017-11-14 18:48:44 +01:00 committed by Benjamin Dauvergne
parent 83d2de7030
commit 26beade166
4 changed files with 226 additions and 1 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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)

View File

@ -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 &copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
# 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

View File

@ -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