0001-lingo-add-possibility-to-compute-extra-fees-16065.patch
combo/apps/lingo/migrations/0029_auto_20170528_1334.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 |
import jsonfield.fields |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('lingo', '0028_tipipaymentformcell'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AlterModelOptions( |
|
16 |
name='basketitem', |
|
17 |
options={'ordering': ['regie', 'extra_fee', 'subject']}, |
|
18 |
), |
|
19 |
migrations.AddField( |
|
20 |
model_name='basketitem', |
|
21 |
name='extra_fee', |
|
22 |
field=models.BooleanField(default=False), |
|
23 |
), |
|
24 |
migrations.AddField( |
|
25 |
model_name='basketitem', |
|
26 |
name='request_data', |
|
27 |
field=jsonfield.fields.JSONField(default=dict, blank=True), |
|
28 |
), |
|
29 |
migrations.AddField( |
|
30 |
model_name='regie', |
|
31 |
name='extra_fees_ws_url', |
|
32 |
field=models.URLField(verbose_name='Webservice URL to compute extra fees', blank=True), |
|
33 |
), |
|
34 |
] |
combo/apps/lingo/models.py | ||
---|---|---|
83 | 83 |
is_default = models.BooleanField(verbose_name=_('Default Regie'), default=False) |
84 | 84 |
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'), |
85 | 85 |
blank=True) |
86 |
extra_fees_ws_url = models.URLField(_('Webservice URL to compute extra fees'), |
|
87 |
blank=True) |
|
86 | 88 |
payment_min_amount = models.DecimalField(_('Minimal payment amount'), |
87 | 89 |
max_digits=7, decimal_places=2, default=0) |
88 | 90 | |
... | ... | |
167 | 169 |
'text': self.label, |
168 | 170 |
'description': self.description} |
169 | 171 | |
172 |
def compute_extra_fees(self, user): |
|
173 |
if not self.extra_fees_ws_url: |
|
174 |
return |
|
175 |
post_data = {'data': []} |
|
176 |
basketitems = BasketItem.objects.filter( |
|
177 |
user=user, regie=self, |
|
178 |
cancellation_date__isnull=True, |
|
179 |
payment_date__isnull=True) |
|
180 |
for basketitem in basketitems.filter(extra_fee=False): |
|
181 |
basketitem_data = { |
|
182 |
'subject': basketitem.subject, |
|
183 |
'source_url': basketitem.source_url, |
|
184 |
'details': basketitem.details, |
|
185 |
'amount': basketitem.amount, |
|
186 |
'request_data': basketitem.request_data |
|
187 |
} |
|
188 |
post_data['data'].append(basketitem_data) |
|
189 |
if not post_data['data']: |
|
190 |
basketitems.filter(extra_fee=True).delete() |
|
191 |
return |
|
192 |
response = requests.post(self.extra_fees_ws_url, remote_service='auto') |
|
193 |
if response.status_code != 200 or response.json().get('err'): |
|
194 |
logger = logging.getLogger(__name__) |
|
195 |
logger.error('failed to compute extra frees (user: %r)', user) |
|
196 |
return |
|
197 |
basketitems.filter(extra_fee=True).delete() |
|
198 |
for extra_fee in response.json().get('data'): |
|
199 |
BasketItem(user=user, regie=self, |
|
200 |
subject=extra_fee.get('subject'), |
|
201 |
amount=extra_fee.get('amount'), |
|
202 |
extra_fee=True, |
|
203 |
user_cancellable=False).save() |
|
204 | ||
170 | 205 | |
171 | 206 |
class BasketItem(models.Model): |
172 | 207 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) |
... | ... | |
176 | 211 |
details = models.TextField(verbose_name=_('Details'), blank=True) |
177 | 212 |
amount = models.DecimalField(verbose_name=_('Amount'), |
178 | 213 |
decimal_places=2, max_digits=8) |
214 |
request_data = JSONField(blank=True) |
|
215 |
extra_fee = models.BooleanField(default=False) |
|
179 | 216 |
user_cancellable = models.BooleanField(default=True) |
180 | 217 |
creation_date = models.DateTimeField(auto_now_add=True) |
181 | 218 |
cancellation_date = models.DateTimeField(null=True) |
182 | 219 |
payment_date = models.DateTimeField(null=True) |
183 | 220 |
notification_date = models.DateTimeField(null=True) |
184 | 221 | |
222 |
class Meta: |
|
223 |
ordering = ['regie', 'extra_fee', 'subject'] |
|
224 | ||
185 | 225 |
def notify(self, status): |
226 |
if not self.source_url: |
|
227 |
return |
|
186 | 228 |
url = self.source_url + 'jump/trigger/%s' % status |
187 | 229 |
message = {'result': 'ok'} |
188 | 230 |
if status == 'paid': |
... | ... | |
200 | 242 |
self.notify('paid') |
201 | 243 |
self.notification_date = timezone.now() |
202 | 244 |
self.save() |
245 |
self.regie.compute_extra_fees(user=self.user) |
|
203 | 246 | |
204 | 247 |
def notify_cancellation(self, notify_origin=False): |
205 | 248 |
if notify_origin: |
206 | 249 |
self.notify('cancelled') |
207 | 250 |
self.cancellation_date = timezone.now() |
208 | 251 |
self.save() |
252 |
self.regie.compute_extra_fees(user=self.user) |
|
209 | 253 | |
210 | 254 |
@property |
211 | 255 |
def total_amount(self): |
combo/apps/lingo/templates/lingo/combo/basket.html | ||
---|---|---|
8 | 8 |
<input type="hidden" name="next_url" value="{{ cell.page.get_online_url }}" /> |
9 | 9 |
<ul> |
10 | 10 |
{% for item in regie_info.items %} |
11 |
<li><a href="{{ item.source_url }}">{{ item.subject }}</a>: {{ item.amount }} €
|
|
11 |
<li><a {% if item.source_url %}href="{{ item.source_url }}{% endif %}">{{ item.subject }}</a>: {{ item.amount }} €
|
|
12 | 12 |
{% if item.user_cancellable %} |
13 | 13 |
<a rel="popup" href="{% url 'lingo-cancel-item' pk=item.id %}">({% trans 'remove' %})</a> |
14 | 14 |
{% endif %} |
combo/apps/lingo/views.py | ||
---|---|---|
114 | 114 |
if extra.get('amount'): |
115 | 115 |
item.amount += self.get_amount(extra['amount']) |
116 | 116 | |
117 |
if 'extra' in request_body: |
|
118 |
item.request_data = request_body.get('extra') |
|
119 |
else: |
|
120 |
item.request_data = request_body |
|
121 | ||
117 | 122 |
try: |
118 | 123 |
if request.GET.get('NameId'): |
119 | 124 |
if UserSAMLIdentifier is None: |
... | ... | |
153 | 158 |
item.source_url = request_body.get('url') or '' |
154 | 159 | |
155 | 160 |
item.save() |
161 |
item.regie.compute_extra_fees(user=item.user) |
|
156 | 162 | |
157 | 163 |
response = HttpResponse(content_type='application/json') |
158 | 164 |
response.write(json.dumps({'result': 'success', 'id': str(item.id)})) |
... | ... | |
297 | 303 |
remote_items_data.append(regie.get_invoice(request.user, item_id)) |
298 | 304 |
remote_items = ','.join([x.id for x in remote_items_data]) |
299 | 305 |
else: |
306 |
regie.compute_extra_fees(user=self.request.user) |
|
300 | 307 |
items = BasketItem.objects.filter(user=self.request.user, |
301 | 308 |
regie=regie, payment_date__isnull=True, |
302 | 309 |
cancellation_date__isnull=True) |
... | ... | |
427 | 434 |
except RuntimeError: |
428 | 435 |
# ignore errors, it should be retried later on if it fails |
429 | 436 |
pass |
437 |
regie.compute_extra_fees(user=transaction.user) |
|
430 | 438 |
if transaction.remote_items: |
431 | 439 |
for item_id in transaction.remote_items.split(','): |
432 | 440 |
remote_item = regie.get_invoice(user=transaction.user, invoice_id=item_id) |
tests/test_lingo_payment.py | ||
---|---|---|
191 | 191 | |
192 | 192 |
url = '%s?email=%s®ie_id=%s' % ( |
193 | 193 |
reverse('api-add-basket-item'), user_email, regie.id) |
194 |
data['extra'] = {'amount': '22.24'} |
|
194 |
data['extra'] = {'amount': '22.24', 'foo': 'bar'}
|
|
195 | 195 |
url = sign_url(url, settings.LINGO_API_SIGN_KEY) |
196 | 196 |
resp = client.post(url, json.dumps(data), content_type='application/json') |
197 | 197 |
assert resp.status_code == 200 |
198 | 198 |
assert json.loads(resp.content)['result'] == 'success' |
199 | 199 |
assert BasketItem.objects.filter(amount=Decimal('22.24')).exists() |
200 | 200 |
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].regie_id == regie.id |
201 |
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].request_data == data['extra'] |
|
201 | 202 | |
202 | 203 |
url = '%s?email=%s®ie_id=%s' % ( |
203 | 204 |
reverse('api-add-basket-item'), user_email, regie.slug) |
... | ... | |
446 | 447 |
resp = client.post(url, content_type='application/json') |
447 | 448 |
assert json.loads(resp.content)['err'] == 1 |
448 | 449 |
assert TransactionOperation.objects.filter(transaction=t1).count() == 1 |
450 | ||
451 |
def test_extra_fees(key, regie, user): |
|
452 |
regie.extra_fees_ws_url = 'http://www.example.net/extra-fees' |
|
453 |
regie.save() |
|
454 | ||
455 |
user_email = 'foo@example.com' |
|
456 |
User.objects.get_or_create(email=user_email) |
|
457 |
amount = 42 |
|
458 |
data = {'amount': amount, 'display_name': 'test amount'} |
|
459 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
460 |
mock_json = mock.Mock() |
|
461 |
mock_json.status_code = 200 |
|
462 |
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '5'}]} |
|
463 |
request.return_value = mock_json |
|
464 |
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key) |
|
465 |
resp = client.post(url, json.dumps(data), content_type='application/json') |
|
466 |
assert resp.status_code == 200 |
|
467 |
assert json.loads(resp.content)['result'] == 'success' |
|
468 |
assert BasketItem.objects.filter(amount=amount).exists() |
|
469 |
assert BasketItem.objects.filter(amount=amount)[0].regie_id == regie.id |
|
470 |
assert BasketItem.objects.filter(amount=5, extra_fee=True).exists() |
|
471 |
assert BasketItem.objects.filter(amount=5, extra_fee=True)[0].regie_id == regie.id |
|
472 | ||
473 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
474 |
mock_json = mock.Mock() |
|
475 |
mock_json.status_code = 200 |
|
476 |
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '7'}]} |
|
477 |
request.return_value = mock_json |
|
478 |
data['amount'] = 43 |
|
479 |
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-add-basket-item'), user_email), key) |
|
480 |
resp = client.post(url, json.dumps(data), content_type='application/json') |
|
481 |
assert resp.status_code == 200 |
|
482 |
assert json.loads(resp.content)['result'] == 'success' |
|
483 |
assert not BasketItem.objects.filter(amount=5, extra_fee=True).exists() |
|
484 |
assert BasketItem.objects.filter(amount=7, extra_fee=True).exists() |
|
485 | ||
486 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
487 |
mock_json = mock.Mock() |
|
488 |
mock_json.status_code = 200 |
|
489 |
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '4'}]} |
|
490 |
request.return_value = mock_json |
|
491 |
url = sign_url('%s?email=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_email), key) |
|
492 |
data = {'basket_item_id': BasketItem.objects.get(amount=43).id} |
|
493 |
resp = client.post(url, json.dumps(data), content_type='application/json') |
|
494 |
assert resp.status_code == 200 |
|
495 |
assert not BasketItem.objects.filter(amount=7, extra_fee=True).exists() |
|
496 |
assert BasketItem.objects.filter(amount=4, extra_fee=True).exists() |
|
497 | ||
498 |
# test payment |
|
499 |
login() |
|
500 | ||
501 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
502 |
mock_json = mock.Mock() |
|
503 |
mock_json.status_code = 200 |
|
504 |
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '2'}]} |
|
505 |
request.return_value = mock_json |
|
506 |
resp = client.post(reverse('lingo-pay'), {'regie': regie.pk}) |
|
507 |
assert resp.status_code == 302 |
|
508 |
location = resp.get('location') |
|
509 |
parsed = urlparse.urlparse(location) |
|
510 |
qs = urlparse.parse_qs(parsed.query) |
|
511 |
transaction_id = qs['transaction_id'][0] |
|
512 |
data = {'transaction_id': transaction_id, 'signed': True, |
|
513 |
'amount': qs['amount'][0], 'ok': True} |
|
514 |
assert data['amount'] == '44.00' |
|
515 | ||
516 |
# call callback with GET |
|
517 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) |
|
518 |
resp = client.get(callback_url, data) |
|
519 |
assert resp.status_code == 200 |
|
520 |
assert Transaction.objects.get(order_id=transaction_id).status == 3 |
|
449 |
- |