0002-lingo-make-payment-related-views-compliant-with-Paym.patch
combo/apps/lingo/urls.py | ||
---|---|---|
49 | 49 |
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'), |
50 | 50 |
url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'), |
51 | 51 |
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'), |
52 |
url(r'^lingo/callback-pb/(?P<pb_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback-pb'), |
|
52 | 53 |
url(r'^lingo/return/(?P<regie_pk>\w+)/$', ReturnView.as_view(), name='lingo-return'), |
54 |
url(r'^lingo/return-pb/(?P<pb_pk>\w+)/$', ReturnView.as_view(), name='lingo-return-pb'), |
|
53 | 55 |
url(r'^manage/lingo/', decorated_includes(manager_required, |
54 | 56 |
include(lingo_manager_urls))), |
55 | 57 |
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/pdf$', |
combo/apps/lingo/views.py | ||
---|---|---|
43 | 43 |
from combo.profile.utils import get_user_from_name_id |
44 | 44 | |
45 | 45 |
from .models import (Regie, BasketItem, Transaction, TransactionOperation, |
46 |
LingoBasketCell, SelfDeclaredInvoicePayment)
|
|
46 |
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend)
|
|
47 | 47 | |
48 |
def get_eopayment_object(request, regie):
|
|
49 |
options = regie.service_options
|
|
48 |
def get_eopayment_object(request, payment_backend):
|
|
49 |
options = payment_backend.service_options
|
|
50 | 50 |
options.update({ |
51 | 51 |
'automatic_return_url': request.build_absolute_uri( |
52 |
reverse('lingo-callback', kwargs={'regie_pk': regie.id})),
|
|
52 |
reverse('lingo-callback-pb', kwargs={'pb_pk': payment_backend.id})),
|
|
53 | 53 |
'normal_return_url': request.build_absolute_uri( |
54 |
reverse('lingo-return', kwargs={'regie_pk': regie.id})),
|
|
54 |
reverse('lingo-return-pb', kwargs={'pb_pk': payment_backend.id})),
|
|
55 | 55 |
}) |
56 |
return eopayment.Payment(regie.service, options)
|
|
56 |
return eopayment.Payment(payment_backend.service, options)
|
|
57 | 57 | |
58 | 58 | |
59 | 59 |
def get_basket_url(): |
... | ... | |
228 | 228 |
request.GET['transaction_id']) |
229 | 229 |
raise Http404 |
230 | 230 | |
231 |
payment = get_eopayment_object(request, transaction.regie) |
|
231 |
payment = get_eopayment_object(request, transaction.regie.payment_backend)
|
|
232 | 232 |
amount = request.GET['amount'] |
233 | 233 | |
234 | 234 |
logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id)) |
... | ... | |
269 | 269 |
request.GET['transaction_id']) |
270 | 270 |
raise Http404 |
271 | 271 | |
272 |
payment = get_eopayment_object(request, transaction.regie) |
|
272 |
payment = get_eopayment_object(request, transaction.regie.payment_backend)
|
|
273 | 273 |
amount = request.GET['amount'] |
274 | 274 | |
275 | 275 |
logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id)) |
... | ... | |
326 | 326 |
transaction.status = 0 |
327 | 327 |
transaction.amount = total_amount |
328 | 328 | |
329 |
payment = get_eopayment_object(request, regie) |
|
329 |
payment = get_eopayment_object(request, regie.payment_backend)
|
|
330 | 330 |
kwargs = { |
331 | 331 |
'email': email, 'first_name': firstname, 'last_name': lastname |
332 | 332 |
} |
... | ... | |
434 | 434 | |
435 | 435 |
class PaymentView(View): |
436 | 436 |
def handle_response(self, request, backend_response, **kwargs): |
437 |
regie = Regie.objects.get(id=kwargs.get('regie_pk')) |
|
438 |
payment = get_eopayment_object(request, regie) |
|
437 |
if 'regie_pk' in kwargs: |
|
438 |
payment_backend = Regie.objects.get(id=kwargs['regie_pk']).payment_backend |
|
439 |
elif 'pb_pk' in kwargs: |
|
440 |
payment_backend = PaymentBackend.objects.get(id=kwargs['pb_pk']) |
|
441 |
else: |
|
442 |
raise Exception("A payment backend or regie primary key must be specified") |
|
443 | ||
444 |
payment = get_eopayment_object(request, payment_backend) |
|
439 | 445 |
logger = logging.getLogger(__name__) |
440 | 446 |
logger.info(u'received payment response: %r', backend_response) |
441 | 447 |
try: |
... | ... | |
484 | 490 |
transaction.status, payment_response.result)) |
485 | 491 | |
486 | 492 |
# check if transaction belongs to right regie |
487 |
if not transaction.regie == regie: |
|
488 |
logger.warning(u'received payment for inappropriate regie ' |
|
489 |
'(expecteds: %s, received: %s)' % (transaction.regie, regie)) |
|
493 |
if not transaction.regie.payment_backend == payment_backend: |
|
494 |
logger.warning(u'received payment for inappropriate payment backend ' |
|
495 |
'(expecteds: %s, received: %s)' % ( |
|
496 |
transaction.regie.payment_backend, payment_backend)) |
|
490 | 497 |
raise PaymentException('Invalid payment regie') |
491 | 498 | |
492 | 499 |
transaction.status = payment_response.result |
... | ... | |
517 | 524 |
except: |
518 | 525 |
# ignore errors, it will be retried later on if it fails |
519 | 526 |
logger.exception('error in sync notification for basket item %s', item.id) |
520 |
regie.compute_extra_fees(user=transaction.user) |
|
527 | ||
521 | 528 |
if transaction.remote_items: |
522 | 529 |
transaction.first_notify_remote_items_of_payments() |
523 | 530 |
tests/test_lingo_payment.py | ||
---|---|---|
18 | 18 |
from webtest import TestApp |
19 | 19 | |
20 | 20 |
from combo.data.models import Page |
21 |
from combo.apps.lingo.models import (Regie, BasketItem, Transaction, |
|
22 |
TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell) |
|
21 |
from combo.apps.lingo.models import ( |
|
22 |
Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell, |
|
23 |
PaymentBackend) |
|
23 | 24 |
from combo.utils import sign_url |
24 | 25 | |
25 | 26 |
from .test_manager import login |
... | ... | |
27 | 28 |
pytestmark = pytest.mark.django_db |
28 | 29 | |
29 | 30 | |
31 |
def get_url(with_pb, view_name, regie): |
|
32 |
if with_pb: |
|
33 |
return reverse(view_name + '-pb', kwargs={'pb_pk': regie.payment_backend.id}) |
|
34 |
return reverse(view_name, kwargs={'regie_pk': regie.id}) |
|
35 | ||
36 | ||
30 | 37 |
@contextmanager |
31 | 38 |
def check_log(caplog, message): |
32 | 39 |
idx = len(caplog.records) |
... | ... | |
37 | 44 | |
38 | 45 |
@pytest.fixture |
39 | 46 |
def regie(): |
47 |
try: |
|
48 |
pb = PaymentBackend.objects.get(slug='test1') |
|
49 |
except PaymentBackend.DoesNotExist: |
|
50 |
pb = PaymentBackend.objects.create( |
|
51 |
label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}) |
|
40 | 52 |
try: |
41 | 53 |
regie = Regie.objects.get(slug='test') |
42 | 54 |
except Regie.DoesNotExist: |
... | ... | |
45 | 57 |
regie.slug = 'test' |
46 | 58 |
regie.description = 'test' |
47 | 59 |
regie.payment_min_amount = Decimal(4.5) |
48 |
regie.service = 'dummy' |
|
49 |
regie.service_options = {'siret': '1234'} |
|
60 |
regie.payment_backend = pb |
|
50 | 61 |
regie.save() |
51 | 62 |
return regie |
52 | 63 | |
... | ... | |
85 | 96 |
return settings.LINGO_API_SIGN_KEY |
86 | 97 | |
87 | 98 |
def test_default_regie(): |
99 |
pb = PaymentBackend.objects.create(label='foo', slug='foo') |
|
88 | 100 |
Regie.objects.all().delete() |
89 |
regie1 = Regie(label='foo', slug='foo') |
|
101 |
regie1 = Regie(label='foo', slug='foo', payment_backend=pb)
|
|
90 | 102 |
regie1.save() |
91 | 103 |
assert bool(regie1.is_default) is True |
92 |
regie2 = Regie(label='bar', slug='bar') |
|
104 |
regie2 = Regie(label='bar', slug='bar', payment_backend=pb)
|
|
93 | 105 |
regie2.save() |
94 | 106 |
assert bool(regie2.is_default) is False |
95 | 107 | |
... | ... | |
121 | 133 |
resp = resp.form.submit() |
122 | 134 |
assert resp.status_code == 302 |
123 | 135 | |
124 |
def test_successfull_items_payment(app, basket_page, regie, user): |
|
136 | ||
137 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
138 |
def test_successfull_items_payment(app, basket_page, regie, user, with_pb): |
|
125 | 139 |
items = {'item1': {'amount': '10.5', 'source_url': 'http://example.org/item/1'}, |
126 | 140 |
'item2': {'amount': '42', 'source_url': 'http://example.org/item/2'}, |
127 | 141 |
'item3': {'amount': '100', 'source_url': 'http://example.org/item/3'}, |
... | ... | |
145 | 159 |
'ok': True, 'reason': 'Paid'} |
146 | 160 |
# make sure return url is the user return URL |
147 | 161 |
assert urlparse.urlparse(qs['return_url'][0]).path.startswith( |
148 |
reverse('lingo-return', kwargs={'regie_pk': regie.id}))
|
|
162 |
reverse('lingo-return-pb', kwargs={'pb_pk': regie.payment_backend.id}))
|
|
149 | 163 |
# simulate successful call to callback URL |
150 | 164 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
151 |
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': regie.id}), params=args)
|
|
165 |
resp = app.get(get_url(with_pb, 'lingo-callback', regie), params=args)
|
|
152 | 166 |
assert resp.status_code == 200 |
153 | 167 |
# simulate successful return URL |
154 | 168 |
resp = app.get(qs['return_url'][0], params=args) |
... | ... | |
158 | 172 |
assert 'Your payment has been succesfully registered.' in resp.text |
159 | 173 | |
160 | 174 |
def test_add_amount_to_basket(app, key, regie, user): |
161 |
other_regie = Regie(label='test2', slug='test2', service='dummy', service_options={'siret': '1234'}) |
|
175 |
pb = PaymentBackend.objects.create( |
|
176 |
label='test2', slug='test2', service='dummy', service_options={'siret': '1234'}) |
|
177 |
other_regie = Regie(label='test2', slug='test2', payment_backend=pb) |
|
162 | 178 |
other_regie.save() |
163 | 179 | |
164 | 180 |
user_email = 'foo@example.com' |
... | ... | |
479 | 495 |
app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=404) |
480 | 496 |
app.post(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=403) |
481 | 497 | |
482 |
def test_payment_callback(app, basket_page, regie, user): |
|
498 | ||
499 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
500 |
def test_payment_callback(app, basket_page, regie, user, with_pb): |
|
483 | 501 |
page = Page(title='xxx', slug='index', template_name='standard') |
484 | 502 |
page.save() |
485 | 503 |
item = BasketItem.objects.create(user=user, regie=regie, |
... | ... | |
497 | 515 |
assert data['amount'] == '10.50' |
498 | 516 | |
499 | 517 |
# call callback with GET |
500 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
|
|
518 |
callback_url = get_url(with_pb, 'lingo-callback', regie)
|
|
501 | 519 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
502 | 520 |
get_resp = app.get(callback_url, params=data) |
503 | 521 |
url = request.call_args[0][1] |
... | ... | |
526 | 544 |
assert Transaction.objects.get(order_id=transaction_id).status == 3 |
527 | 545 | |
528 | 546 |
# call return view |
529 |
get_resp = app.get(reverse('lingo-return', kwargs={'regie_pk': regie.pk}), params=data) |
|
547 |
return_url = get_url(with_pb, 'lingo-return', regie) |
|
548 |
get_resp = app.get(return_url, params=data) |
|
530 | 549 |
assert get_resp.status_code == 302 |
531 | 550 |
resp = app.get(get_resp['Location']) |
532 | 551 |
assert 'Your payment has been succesfully registered.' in resp.text |
533 | 552 | |
534 |
def test_payment_callback_no_regie(app, basket_page, regie, user): |
|
553 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
554 |
def test_payment_callback_no_regie(app, basket_page, regie, user, with_pb): |
|
535 | 555 |
item = BasketItem.objects.create(user=user, regie=regie, |
536 | 556 |
subject='test_item', amount='10.5', |
537 | 557 |
source_url='http://example.org/testitem/') |
... | ... | |
547 | 567 |
assert data['amount'] == '10.50' |
548 | 568 | |
549 | 569 |
# call callback with GET |
550 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
|
|
570 |
callback_url = get_url(with_pb, 'lingo-callback', regie)
|
|
551 | 571 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
552 | 572 |
get_resp = app.get(callback_url, params=data) |
553 | 573 |
url = request.call_args[0][1] |
... | ... | |
569 | 589 |
'amount': qs['amount'][0], 'ok': True} |
570 | 590 |
assert data['amount'] == '11.50' |
571 | 591 | |
572 |
def test_nonexisting_transaction(app, regie, user): |
|
592 | ||
593 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
594 |
def test_nonexisting_transaction(app, regie, user, with_pb): |
|
573 | 595 |
app = login(app) |
574 | 596 |
data = {'transaction_id': 'unknown', 'signed': True, |
575 | 597 |
'amount': '23', 'ok': True} |
576 | 598 | |
577 | 599 |
# call callback with GET |
578 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
|
|
600 |
callback_url = get_url(with_pb, 'lingo-callback', regie)
|
|
579 | 601 |
app.get(callback_url, params=data, status=404) |
580 | 602 | |
581 |
def test_payment_callback_waiting(app, basket_page, regie, user): |
|
603 | ||
604 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
605 |
def test_payment_callback_waiting(app, basket_page, regie, user, with_pb): |
|
582 | 606 |
item = BasketItem.objects.create(user=user, regie=regie, |
583 | 607 |
subject='test_item', amount='10.5', |
584 | 608 |
source_url='http://example.org/testitem/') |
... | ... | |
594 | 618 |
assert data['amount'] == '10.50' |
595 | 619 | |
596 | 620 |
# callback with WAITING state |
597 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
|
|
621 |
callback_url = get_url(with_pb, 'lingo-callback', regie)
|
|
598 | 622 |
resp = app.get(callback_url, params=data) |
599 | 623 |
assert resp.status_code == 200 |
600 | 624 |
assert Transaction.objects.get(order_id=transaction_id).status == eopayment.WAITING |
... | ... | |
615 | 639 |
assert BasketItem.objects.get(id=item.id).payment_date |
616 | 640 |
assert BasketItem.get_items_to_be_paid(user).count() == 0 |
617 | 641 | |
618 |
def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user): |
|
642 | ||
643 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
644 |
def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user, with_pb): |
|
619 | 645 |
item = BasketItem.objects.create(user=user, regie=regie, |
620 | 646 |
subject='test_item', amount='10.5', |
621 | 647 |
source_url='http://example.org/testitem/') |
... | ... | |
632 | 658 | |
633 | 659 |
# call return with unsigned POST |
634 | 660 |
with check_log(caplog, 'received unsigned payment'): |
635 |
return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id})
|
|
661 |
return_url = get_url(with_pb, 'lingo-return', regie)
|
|
636 | 662 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
637 | 663 |
get_resp = app.post(return_url, params=data) |
638 | 664 |
assert request.call_count == 0 |
... | ... | |
654 | 680 | |
655 | 681 |
# call return with signed POST |
656 | 682 |
data['signed'] = True |
657 |
return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id})
|
|
683 |
return_url = get_url(with_pb, 'lingo-return', regie)
|
|
658 | 684 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
659 | 685 |
get_resp = app.post(return_url, params=data) |
660 | 686 |
url = request.call_args[0][1] |
... | ... | |
827 | 853 |
assert resp.status_code == 200 |
828 | 854 |
assert Transaction.objects.get(order_id=transaction_id).status == 3 |
829 | 855 | |
830 |
def test_payment_callback_error(app, basket_page, regie, user): |
|
856 | ||
857 |
@pytest.mark.parametrize('with_pb', [False, True]) |
|
858 |
def test_payment_callback_error(app, basket_page, regie, user, with_pb): |
|
831 | 859 |
item = BasketItem.objects.create(user=user, regie=regie, |
832 | 860 |
subject='test_item', amount='10.5', |
833 | 861 |
source_url='http://example.org/testitem/') |
... | ... | |
843 | 871 |
assert data['amount'] == '10.50' |
844 | 872 | |
845 | 873 |
# call callback with GET |
846 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
|
|
874 |
callback_url = get_url(with_pb, 'lingo-callback', regie)
|
|
847 | 875 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
848 | 876 |
mock_response = mock.Mock() |
849 | 877 |
def kaboom(): |
tests/test_lingo_remote_regie.py | ||
---|---|---|
20 | 20 |
from combo.utils import check_query, aes_hex_encrypt |
21 | 21 |
from combo.data.models import Page |
22 | 22 |
from combo.apps.lingo.models import (Regie, ActiveItems, ItemsHistory, SelfDeclaredInvoicePayment, |
23 |
Transaction, BasketItem) |
|
23 |
Transaction, BasketItem, PaymentBackend)
|
|
24 | 24 | |
25 | 25 |
pytestmark = pytest.mark.django_db |
26 | 26 | |
... | ... | |
55 | 55 | |
56 | 56 |
@pytest.fixture |
57 | 57 |
def remote_regie(): |
58 |
try: |
|
59 |
pb = PaymentBackend.objects.get(slug='test1') |
|
60 |
except PaymentBackend.DoesNotExist: |
|
61 |
pb = PaymentBackend.objects.create( |
|
62 |
label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}) |
|
58 | 63 |
try: |
59 | 64 |
regie = Regie.objects.get(slug='remote') |
60 | 65 |
except Regie.DoesNotExist: |
... | ... | |
63 | 68 |
regie.slug = 'remote' |
64 | 69 |
regie.description = 'remote' |
65 | 70 |
regie.payment_min_amount = Decimal(2.0) |
66 |
regie.service = 'dummy' |
|
67 |
regie.service_options = {'siret': '1234'} |
|
71 |
regie.payment_backend = pb |
|
68 | 72 |
regie.webservice_url = 'http://example.org/regie' # is_remote |
69 | 73 |
regie.save() |
70 | 74 |
return regie |
... | ... | |
204 | 208 |
'ok': True, 'reason': 'Paid'} |
205 | 209 |
# make sure return url is the user return URL |
206 | 210 |
assert urlparse.urlparse(qs['return_url'][0]).path.startswith( |
207 |
reverse('lingo-return', kwargs={'regie_pk': remote_regie.id}))
|
|
211 |
reverse('lingo-return-pb', kwargs={'pb_pk': remote_regie.payment_backend.id}))
|
|
208 | 212 |
# simulate successful return URL |
209 | 213 |
resp = app.get(qs['return_url'][0], params=args) |
210 | 214 |
assert resp.status_code == 302 |
... | ... | |
326 | 330 |
'ok': True, 'reason': 'Paid'} |
327 | 331 |
# make sure return url is the user return URL |
328 | 332 |
assert urlparse.urlparse(qs['return_url'][0]).path.startswith( |
329 |
reverse('lingo-return', kwargs={'regie_pk': remote_regie.id}))
|
|
333 |
reverse('lingo-return-pb', kwargs={'pb_pk': remote_regie.payment_backend.id}))
|
|
330 | 334 |
# simulate payment failure |
331 | 335 |
mock_get.side_effect = ConnectionError('where is my hostname?') |
332 | 336 |
resp = app.get(qs['return_url'][0], params=args) |
333 |
- |