0001-lingo-notify-new-remote-invoices-13122.patch
combo/apps/lingo/management/commands/notify_new_remote_invoices.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# |
|
3 |
# lingo - basket and payment system |
|
4 |
# Copyright (C) 2018 Entr'ouvert |
|
5 |
# |
|
6 |
# This program is free software: you can redistribute it and/or modify it |
|
7 |
# under the terms of the GNU Affero General Public License as published |
|
8 |
# by the Free Software Foundation, either version 3 of the License, or |
|
9 |
# (at your option) any later version. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, |
|
12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
# GNU Affero General Public License for more details. |
|
15 |
# |
|
16 |
# You should have received a copy of the GNU Affero General Public License |
|
17 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18 | ||
19 |
import logging |
|
20 | ||
21 |
from django.core.management.base import BaseCommand |
|
22 | ||
23 |
from combo.apps.lingo.models import Regie |
|
24 | ||
25 | ||
26 |
class Command(BaseCommand): |
|
27 | ||
28 |
def handle(self, *args, **kwargs): |
|
29 |
logger = logging.getLogger(__name__) |
|
30 |
for regie in Regie.objects.exclude(webservice_url=''): |
|
31 |
try: |
|
32 |
regie.notify_new_remote_invoices() |
|
33 |
except Exception, e: |
|
34 |
logger.exception('error while notifying new remote invoices: %s', e) |
combo/apps/lingo/models.py | ||
---|---|---|
32 | 32 |
from django.db import models |
33 | 33 |
from django.forms import models as model_forms, Select |
34 | 34 |
from django.utils.translation import ugettext_lazy as _ |
35 |
from django.utils import timezone |
|
35 |
from django.utils import timezone, dateparse
|
|
36 | 36 |
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied |
37 | 37 |
from django.utils.http import urlencode |
38 | 38 | |
39 |
from django.contrib.auth.models import User |
|
40 | ||
39 | 41 |
from combo.data.fields import RichTextField |
40 | 42 |
from combo.data.models import CellBase |
41 | 43 |
from combo.data.library import register_cell_class |
42 | 44 |
from combo.utils import NothingInCacheException, aes_hex_encrypt, requests |
45 |
from combo.apps.notifications.models import Notification |
|
43 | 46 | |
44 | 47 |
EXPIRED = 9999 |
45 | 48 | |
... | ... | |
203 | 206 |
extra_fee=True, |
204 | 207 |
user_cancellable=False).save() |
205 | 208 | |
209 |
def notify_new_remote_invoices(self): |
|
210 |
if not self.is_remote(): |
|
211 |
return |
|
212 | ||
213 |
logger = logging.getLogger(__name__) |
|
214 |
url = self.webservice_url + '/users/with-pending-invoices/' |
|
215 |
response = requests.get(url, remote_service='auto', cache_duration=0, |
|
216 |
log_errors=False) |
|
217 |
if not response.ok: |
|
218 |
return |
|
219 | ||
220 |
data = response.json()['data'] |
|
221 |
if not data: |
|
222 |
return |
|
223 |
for uuid, items in data.iteritems(): |
|
224 |
try: |
|
225 |
user = User.objects.get(username=uuid) |
|
226 |
except User.DoesNotExist: |
|
227 |
logger.warning('Invoices available for unknown user: %s', uuid) |
|
228 |
continue |
|
229 |
for invoice in items['invoices']: |
|
230 |
invoice_id = 'invoice-%s-%s' % (self.slug, invoice['id']) |
|
231 |
invoice_creation_date = timezone.make_aware(dateparse.parse_datetime(invoice['created'])) |
|
232 |
invoice_pay_limit_date = timezone.make_aware(dateparse.parse_datetime(invoice['pay_limit_date'])) |
|
233 |
notification_end_timestamp = invoice_creation_date + timezone.timedelta(days=settings.LINGO_NEW_INVOICES_NOTIFICATION_DELAY) |
|
234 |
if invoice_pay_limit_date < notification_end_timestamp: |
|
235 |
notification_end_timestamp = invoice_pay_limit_date |
|
236 |
notification_id, created = Notification.notify(user, _('Invoice %s to pay') % invoice['label'], id=invoice_id, |
|
237 |
end_timestamp=notification_end_timestamp) |
|
238 |
if not created: |
|
239 |
notification = Notification.objects.filter_by_id(invoice_id).filter(user=user).last() |
|
240 |
if notification.end_timestamp < timezone.now(): |
|
241 |
remind_id = 'remind-%s' % invoice_id |
|
242 |
Notification.notify(user, _('Reminder: invoice %s to pay') % invoice['label'], |
|
243 |
id=remind_id, end_timestamp=notification_end_timestamp) |
|
244 | ||
206 | 245 | |
207 | 246 |
class BasketItem(models.Model): |
208 | 247 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) |
combo/settings.py | ||
---|---|---|
279 | 279 |
# default combo map attribution |
280 | 280 |
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>' |
281 | 281 | |
282 |
# default delay for invoice payment notifications in days |
|
283 |
LINGO_NEW_INVOICES_NOTIFICATION_DELAY = 10 |
|
284 | ||
282 | 285 |
# timeout used in python-requests call, in seconds |
283 | 286 |
# we use 28s by default: timeout just before web server, which is usually 30s |
284 | 287 |
REQUESTS_TIMEOUT = 28 |
tests/test_notification.py | ||
---|---|---|
1 | 1 |
import json |
2 | 2 | |
3 |
import mock |
|
3 | 4 |
import pytest |
5 |
from decimal import Decimal |
|
4 | 6 | |
5 | 7 |
from django.contrib.auth.models import User |
6 | 8 |
from django.test.client import RequestFactory |
9 |
from django.test import override_settings |
|
7 | 10 |
from django.utils.timezone import timedelta, now |
8 | 11 |
from django.core.urlresolvers import reverse |
9 | 12 | |
... | ... | |
11 | 14 | |
12 | 15 |
from combo.data.models import Page |
13 | 16 |
from combo.apps.notifications.models import Notification, NotificationsCell |
17 |
from combo.apps.lingo.models import Regie |
|
14 | 18 | |
15 | 19 |
pytestmark = pytest.mark.django_db |
16 | 20 | |
... | ... | |
38 | 42 |
resp = client.post('/login/', {'username': username, 'password': password}) |
39 | 43 |
assert resp.status_code == 302 |
40 | 44 | |
45 |
@pytest.fixture |
|
46 |
def regie(): |
|
47 |
try: |
|
48 |
regie = Regie.objects.get(slug='remote') |
|
49 |
except Regie.DoesNotExist: |
|
50 |
regie = Regie() |
|
51 |
regie.label = 'Remote' |
|
52 |
regie.slug = 'remote' |
|
53 |
regie.description = 'remote' |
|
54 |
regie.payment_min_amount = Decimal(2.0) |
|
55 |
regie.service = 'dummy' |
|
56 |
regie.save() |
|
57 |
return regie |
|
41 | 58 | |
42 | 59 |
def test_notification_api(user, user2): |
43 | 60 |
id_notifoo, created = Notification.notify(user, 'notifoo') |
... | ... | |
236 | 253 |
kwargs={'notification_id': 'noti1'}) == '/api/notification/ack/noti1/' |
237 | 254 |
assert reverse('api-notification-forget', |
238 | 255 |
kwargs={'notification_id': 'noti1'}) == '/api/notification/forget/noti1/' |
256 | ||
257 |
@mock.patch('combo.apps.lingo.models.requests.get') |
|
258 |
def test_notify_remote_items(mock_get, app, user, user2, regie, caplog): |
|
259 |
datetime_format = '%Y-%m-%dT%H:%M:%S' |
|
260 |
invoice_now = now() |
|
261 |
creation_date = (invoice_now - timedelta(days=1)).strftime(datetime_format) |
|
262 |
pay_limit_date = (invoice_now + timedelta(days=30)).strftime(datetime_format) |
|
263 |
FAKE_PENDING_INVOICES = { |
|
264 |
"data" : {"admin": {"invoices": [{'id': '01', 'label': '010101', |
|
265 |
'created': creation_date, 'pay_limit_date': pay_limit_date}]}, |
|
266 |
'admin2': {'invoices': [{'id': '02', 'label': '020202', |
|
267 |
'created': creation_date, 'pay_limit_date': pay_limit_date}]}, |
|
268 |
'foo': {'invoices': [{'id': 'O3', 'label': '030303', |
|
269 |
'created': creation_date, 'pay_limite_date': pay_limit_date}]} |
|
270 |
} |
|
271 |
} |
|
272 |
mock_response = mock.Mock(status_code=200, content=json.dumps(FAKE_PENDING_INVOICES)) |
|
273 |
mock_response.json.return_value = FAKE_PENDING_INVOICES |
|
274 |
mock_get.return_value = mock_response |
|
275 |
regie.notify_new_remote_invoices() |
|
276 |
assert mock_get.call_count == 0 |
|
277 |
regie.webservice_url = 'http://example.org/regie' # is_remote |
|
278 |
regie.save() |
|
279 |
with override_settings(LINGO_NEW_INVOICES_NOTIFICATION_DELAY=0): |
|
280 |
regie.notify_new_remote_invoices() |
|
281 | ||
282 |
assert Notification.objects.count() == 2 |
|
283 | ||
284 |
# create remind notifications |
|
285 |
regie.notify_new_remote_invoices() |
|
286 |
assert Notification.objects.count() == 4 |
|
287 | ||
288 |
assert len(caplog.records) == 2 |
|
289 | ||
290 | ||
291 |
@mock.patch('combo.apps.lingo.models.requests.get') |
|
292 |
def test_notify_remote_items_expiring_shortly(mock_get, app, user, user2, regie): |
|
293 |
datetime_format = '%Y-%m-%dT%H:%M:%S' |
|
294 |
invoice_now = now() |
|
295 |
creation_date = (invoice_now - timedelta(1)).strftime(datetime_format) |
|
296 |
pay_limit_date = (invoice_now + timedelta(days=5)).strftime(datetime_format) |
|
297 |
SHORT_PENDING_INVOICES = { |
|
298 |
"data" : {"admin": {"invoices": [{'id': '03', 'label': '030303', |
|
299 |
'created': creation_date, |
|
300 |
'pay_limit_date': pay_limit_date}]}, |
|
301 |
'admin2': {'invoices': [{'id': '04', 'label': '040404', |
|
302 |
'created': creation_date, |
|
303 |
'pay_limit_date': pay_limit_date}]}, |
|
304 |
} |
|
305 |
} |
|
306 |
mock_response = mock.Mock(status_code=200, content=json.dumps(SHORT_PENDING_INVOICES)) |
|
307 |
mock_response.json.return_value = SHORT_PENDING_INVOICES |
|
308 |
mock_get.return_value = mock_response |
|
309 |
regie.webservice_url = 'http://example.org/regie' |
|
310 |
regie.save() |
|
311 |
regie.notify_new_remote_invoices() |
|
312 | ||
313 |
assert Notification.objects.count() == 2 |
|
314 |
for notification in Notification.objects.all(): |
|
315 |
assert notification.end_timestamp.strftime(datetime_format) == pay_limit_date |
|
239 |
- |