lingo: notify new remote invoices (#13122)
This commit is contained in:
parent
83d2de7030
commit
26beade166
|
@ -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)
|
|
@ -32,14 +32,17 @@ from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import models as model_forms, Select
|
from django.forms import models as model_forms, Select
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from combo.data.fields import RichTextField
|
from combo.data.fields import RichTextField
|
||||||
from combo.data.models import CellBase
|
from combo.data.models import CellBase
|
||||||
from combo.data.library import register_cell_class
|
from combo.data.library import register_cell_class
|
||||||
from combo.utils import NothingInCacheException, aes_hex_encrypt, requests
|
from combo.utils import NothingInCacheException, aes_hex_encrypt, requests
|
||||||
|
from combo.apps.notifications.models import Notification
|
||||||
|
|
||||||
EXPIRED = 9999
|
EXPIRED = 9999
|
||||||
|
|
||||||
|
@ -203,6 +206,71 @@ class Regie(models.Model):
|
||||||
extra_fee=True,
|
extra_fee=True,
|
||||||
user_cancellable=False).save()
|
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):
|
class BasketItem(models.Model):
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
|
||||||
|
|
|
@ -285,6 +285,9 @@ COMBO_MAP_TILE_URLTEMPLATE = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
# default combo map attribution
|
# default combo map attribution
|
||||||
COMBO_MAP_ATTRIBUTION = 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
|
COMBO_MAP_ATTRIBUTION = 'Map data © <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
|
# timeout used in python-requests call, in seconds
|
||||||
# we use 28s by default: timeout just before web server, which is usually 30s
|
# we use 28s by default: timeout just before web server, which is usually 30s
|
||||||
REQUESTS_TIMEOUT = 28
|
REQUESTS_TIMEOUT = 28
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -11,6 +13,7 @@ from django.test import Client
|
||||||
|
|
||||||
from combo.data.models import Page
|
from combo.data.models import Page
|
||||||
from combo.apps.notifications.models import Notification, NotificationsCell
|
from combo.apps.notifications.models import Notification, NotificationsCell
|
||||||
|
from combo.apps.lingo.models import Regie, ActiveItems
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -42,6 +45,21 @@ def login(username='admin', password='admin'):
|
||||||
assert resp.status_code == 302
|
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):
|
def test_notification_api(user, user2):
|
||||||
notification = Notification.notify(user, 'notifoo')
|
notification = Notification.notify(user, 'notifoo')
|
||||||
assert Notification.objects.count() == 1
|
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'})
|
result = notify({'summary': 'foo', 'id': 'foo:foo', 'origin': 'bar'})
|
||||||
assert result['err'] == 0
|
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
|
||||||
|
|
Loading…
Reference in New Issue