2016-02-25 11:43:28 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
2015-02-08 15:14:58 +01:00
|
|
|
# lingo - basket and payment system
|
|
|
|
# Copyright (C) 2015 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/>.
|
2015-02-08 11:32:29 +01:00
|
|
|
|
2015-12-02 11:07:49 +01:00
|
|
|
from decimal import Decimal, ROUND_HALF_UP
|
2015-02-08 15:14:58 +01:00
|
|
|
import json
|
2016-05-06 15:20:16 +02:00
|
|
|
import logging
|
2016-03-08 15:35:55 +01:00
|
|
|
import requests
|
2015-02-08 15:14:58 +01:00
|
|
|
|
2015-02-08 16:04:03 +01:00
|
|
|
from django.contrib.auth.models import User
|
2016-10-20 10:04:59 +02:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
2015-03-06 10:47:34 +01:00
|
|
|
from django.core.urlresolvers import reverse
|
2016-05-06 15:20:16 +02:00
|
|
|
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
|
2016-02-19 11:13:37 +01:00
|
|
|
from django.http import HttpResponseForbidden, Http404
|
2015-03-06 10:47:34 +01:00
|
|
|
from django.template.response import TemplateResponse
|
2015-03-07 14:01:24 +01:00
|
|
|
from django.utils import timezone
|
2016-10-20 10:04:59 +02:00
|
|
|
from django.utils.encoding import force_text
|
2015-02-08 16:04:03 +01:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
2016-03-08 15:35:55 +01:00
|
|
|
from django.views.generic import View, DetailView, ListView, TemplateView
|
2015-12-31 16:16:04 +01:00
|
|
|
from django.conf import settings
|
2016-02-15 19:02:14 +01:00
|
|
|
from django.contrib import messages
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2016-06-13 10:10:30 +02:00
|
|
|
from django.db.transaction import atomic
|
2016-06-13 10:52:19 +02:00
|
|
|
from django.utils.encoding import smart_text
|
2015-02-08 15:14:58 +01:00
|
|
|
|
2015-03-05 17:02:52 +01:00
|
|
|
import eopayment
|
|
|
|
|
2018-04-03 14:17:24 +02:00
|
|
|
from combo.data.models import Page
|
2016-11-09 01:14:50 +01:00
|
|
|
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError
|
2015-12-31 16:16:04 +01:00
|
|
|
|
2017-01-02 12:12:41 +01:00
|
|
|
if 'mellon' in settings.INSTALLED_APPS:
|
2015-05-12 17:34:48 +02:00
|
|
|
from mellon.models import UserSAMLIdentifier
|
2017-01-02 12:12:41 +01:00
|
|
|
else:
|
2015-05-12 17:34:48 +02:00
|
|
|
UserSAMLIdentifier = None
|
|
|
|
|
2016-08-21 12:53:03 +02:00
|
|
|
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
|
2016-10-20 10:04:59 +02:00
|
|
|
LingoBasketCell, SelfDeclaredInvoicePayment)
|
2015-02-08 15:14:58 +01:00
|
|
|
|
2016-02-16 19:43:14 +01:00
|
|
|
def get_eopayment_object(request, regie):
|
|
|
|
options = regie.service_options
|
|
|
|
options.update({
|
|
|
|
'automatic_return_url': request.build_absolute_uri(
|
|
|
|
reverse('lingo-callback', kwargs={'regie_pk': regie.id})),
|
|
|
|
'normal_return_url': request.build_absolute_uri(
|
|
|
|
reverse('lingo-return', kwargs={'regie_pk': regie.id})),
|
|
|
|
})
|
|
|
|
return eopayment.Payment(regie.service, options)
|
|
|
|
|
|
|
|
|
2016-03-08 15:35:55 +01:00
|
|
|
def get_basket_url():
|
|
|
|
return LingoBasketCell.objects.all()[0].page.get_online_url()
|
|
|
|
|
|
|
|
|
2016-11-09 01:14:50 +01:00
|
|
|
def lingo_check_request_signature(request):
|
2017-06-29 18:55:27 +02:00
|
|
|
keys = []
|
|
|
|
if getattr(settings, 'LINGO_API_SIGN_KEY', None):
|
|
|
|
keys = [settings.LINGO_API_SIGN_KEY]
|
|
|
|
return check_request_signature(request, keys=keys)
|
2016-09-06 14:33:37 +02:00
|
|
|
|
|
|
|
|
2015-02-08 15:14:58 +01:00
|
|
|
class RegiesApiView(ListView):
|
|
|
|
model = Regie
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
data = {'data': [x.as_api_dict() for x in self.get_queryset()]}
|
|
|
|
json_str = json.dumps(data)
|
|
|
|
if 'jsonpCallback' in request.GET:
|
|
|
|
json_str = '%s(%s);' % (request.GET['jsonpCallback'], json_str)
|
|
|
|
response.write(json_str)
|
|
|
|
return response
|
2015-02-08 16:04:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
class AddBasketItemApiView(View):
|
|
|
|
http_method_names = ['post', 'options']
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(AddBasketItemApiView, self).dispatch(*args, **kwargs)
|
|
|
|
|
2015-12-02 11:07:49 +01:00
|
|
|
def get_amount(self, amount):
|
|
|
|
if isinstance(amount, list):
|
2015-12-22 14:37:37 +01:00
|
|
|
d = Decimal(sum([Decimal(a) for a in amount]))
|
2015-12-02 11:07:49 +01:00
|
|
|
else:
|
|
|
|
d = Decimal(amount)
|
|
|
|
return d.quantize(Decimal('0.01'), ROUND_HALF_UP)
|
|
|
|
|
2015-02-08 16:04:03 +01:00
|
|
|
def post(self, request, *args, **kwargs):
|
2016-11-09 01:14:50 +01:00
|
|
|
if not lingo_check_request_signature(request):
|
2015-12-31 16:16:04 +01:00
|
|
|
return HttpResponseForbidden()
|
2015-03-05 16:02:32 +01:00
|
|
|
|
|
|
|
request_body = json.loads(self.request.body)
|
2015-12-02 11:07:49 +01:00
|
|
|
extra = request_body.get('extra', {})
|
|
|
|
|
2016-03-09 16:01:45 +01:00
|
|
|
if not 'amount' in request.GET and not 'amount' in request_body and \
|
|
|
|
not 'amount' in extra:
|
|
|
|
raise Exception('missing amount parameter')
|
|
|
|
|
2015-12-02 11:07:49 +01:00
|
|
|
item = BasketItem(amount=0)
|
2015-12-22 14:37:37 +01:00
|
|
|
item.amount = self.get_amount(request.GET.getlist('amount'))
|
2015-12-02 11:07:49 +01:00
|
|
|
|
|
|
|
if request_body.get('amount'):
|
|
|
|
item.amount += self.get_amount(request_body['amount'])
|
2015-03-05 16:02:32 +01:00
|
|
|
|
2015-12-02 11:07:49 +01:00
|
|
|
if extra.get('amount'):
|
|
|
|
item.amount += self.get_amount(extra['amount'])
|
2015-02-08 16:04:03 +01:00
|
|
|
|
2017-05-28 12:47:24 +02:00
|
|
|
if 'extra' in request_body:
|
|
|
|
item.request_data = request_body.get('extra')
|
|
|
|
else:
|
|
|
|
item.request_data = request_body
|
|
|
|
|
2015-03-05 16:02:32 +01:00
|
|
|
try:
|
|
|
|
if request.GET.get('NameId'):
|
2015-05-12 17:34:48 +02:00
|
|
|
if UserSAMLIdentifier is None:
|
|
|
|
raise Exception('missing mellon?')
|
|
|
|
try:
|
|
|
|
user = UserSAMLIdentifier.objects.get(name_id=request.GET.get('NameId')).user
|
|
|
|
except UserSAMLIdentifier.DoesNotExist:
|
|
|
|
raise Exception('unknown name id')
|
2015-03-05 16:02:32 +01:00
|
|
|
elif request.GET.get('email'):
|
|
|
|
user = User.objects.get(email=request.GET.get('email'))
|
|
|
|
else:
|
|
|
|
raise Exception('no user specified')
|
|
|
|
except User.DoesNotExist:
|
|
|
|
raise Exception('unknown user')
|
|
|
|
|
|
|
|
item.user = user
|
|
|
|
if request.GET.get('regie_id'):
|
2016-11-10 20:22:09 +01:00
|
|
|
try:
|
|
|
|
item.regie = Regie.objects.get(slug=request.GET.get('regie_id'))
|
|
|
|
except Regie.DoesNotExist:
|
|
|
|
try:
|
|
|
|
item.regie = Regie.objects.get(id=int(request.GET.get('regie_id')))
|
|
|
|
except (ValueError, Regie.DoesNotExist):
|
|
|
|
return HttpResponseBadRequest('Unknown regie')
|
2015-03-05 16:02:32 +01:00
|
|
|
else:
|
2016-09-27 17:06:17 +02:00
|
|
|
try:
|
|
|
|
item.regie = Regie.objects.get(is_default=True)
|
|
|
|
except Regie.DoesNotExist:
|
|
|
|
# if there's no default regie, use the first one we get from
|
|
|
|
# the database...
|
|
|
|
item.regie = Regie.objects.all()[0]
|
2015-03-05 16:02:32 +01:00
|
|
|
|
2016-05-31 15:40:31 +02:00
|
|
|
if request.GET.get('cancellable') == 'no':
|
|
|
|
item.user_cancellable = False
|
|
|
|
|
2015-03-05 16:02:32 +01:00
|
|
|
item.subject = request_body.get('display_name')
|
2017-05-28 14:17:48 +02:00
|
|
|
item.source_url = request_body.get('url') or ''
|
2015-02-08 16:04:03 +01:00
|
|
|
|
|
|
|
item.save()
|
2017-05-28 12:47:24 +02:00
|
|
|
item.regie.compute_extra_fees(user=item.user)
|
2015-03-05 16:02:32 +01:00
|
|
|
|
2016-05-26 13:04:47 +02:00
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
response.write(json.dumps({'result': 'success', 'id': str(item.id)}))
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
class RemoveBasketItemApiView(View):
|
|
|
|
http_method_names = ['post', 'options']
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(RemoveBasketItemApiView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2016-11-09 01:14:50 +01:00
|
|
|
if not lingo_check_request_signature(request):
|
2016-05-26 13:04:47 +02:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
request_body = json.loads(self.request.body)
|
|
|
|
|
|
|
|
if not 'basket_item_id' in request_body:
|
|
|
|
raise Exception('missing basket_item_id parameter')
|
|
|
|
|
|
|
|
try:
|
|
|
|
if request.GET.get('NameId'):
|
|
|
|
if UserSAMLIdentifier is None:
|
|
|
|
raise Exception('missing mellon?')
|
|
|
|
try:
|
|
|
|
user = UserSAMLIdentifier.objects.get(name_id=request.GET.get('NameId')).user
|
|
|
|
except UserSAMLIdentifier.DoesNotExist:
|
|
|
|
raise Exception('unknown name id')
|
|
|
|
elif request.GET.get('email'):
|
|
|
|
user = User.objects.get(email=request.GET.get('email'))
|
|
|
|
else:
|
|
|
|
raise Exception('no user specified')
|
|
|
|
except User.DoesNotExist:
|
|
|
|
raise Exception('unknown user')
|
|
|
|
|
|
|
|
item = BasketItem.objects.get(id=request_body.get('basket_item_id'),
|
|
|
|
user=user, cancellation_date__isnull=True)
|
2017-04-07 17:44:42 +02:00
|
|
|
notify_origin = bool(request_body.get('notify', 'false') == 'true')
|
|
|
|
item.notify_cancellation(notify_origin=notify_origin)
|
2016-05-26 13:04:47 +02:00
|
|
|
|
2015-02-08 16:04:03 +01:00
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
response.write(json.dumps({'result': 'success'}))
|
|
|
|
return response
|
2015-03-05 17:02:52 +01:00
|
|
|
|
2016-05-26 13:04:47 +02:00
|
|
|
|
2016-08-21 12:53:03 +02:00
|
|
|
class ValidateTransactionApiView(View):
|
|
|
|
http_method_names = ['post', 'options']
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(ValidateTransactionApiView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2016-11-09 01:14:50 +01:00
|
|
|
if not lingo_check_request_signature(request):
|
2016-08-21 12:53:03 +02:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
|
|
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
|
|
|
|
except Transaction.DoesNotExist:
|
|
|
|
logger.warning(u'received validate request for unknown transaction %s',
|
|
|
|
request.GET['transaction_id'])
|
|
|
|
raise Http404
|
|
|
|
|
|
|
|
payment = get_eopayment_object(request, transaction.regie)
|
|
|
|
amount = request.GET['amount']
|
|
|
|
|
|
|
|
logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id))
|
|
|
|
try:
|
|
|
|
result = payment.backend.validate(Decimal(amount), transaction.bank_data)
|
|
|
|
except eopayment.ResponseError as e:
|
|
|
|
logger.error(u'failed in validation operation: %s', e)
|
|
|
|
response = HttpResponse(content_type='application/json')
|
2018-07-25 14:28:05 +02:00
|
|
|
response.write(json.dumps({'err': 1, 'e': force_text(e)}))
|
2016-08-21 12:53:03 +02:00
|
|
|
return response
|
|
|
|
|
|
|
|
logger.info(u'bank validation result: %r', result)
|
|
|
|
operation = TransactionOperation(transaction=transaction,
|
|
|
|
kind='validation', amount=Decimal(amount), bank_result=result)
|
|
|
|
operation.save()
|
|
|
|
|
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
response.write(json.dumps({'err': 0, 'extra': result}))
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
class CancelTransactionApiView(View):
|
|
|
|
http_method_names = ['post', 'options']
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(CancelTransactionApiView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2016-11-09 01:14:50 +01:00
|
|
|
if not lingo_check_request_signature(request):
|
2016-08-21 12:53:03 +02:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
|
|
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
|
|
|
|
except Transaction.DoesNotExist:
|
|
|
|
logger.warning(u'received validate request for unknown transaction %s',
|
|
|
|
request.GET['transaction_id'])
|
|
|
|
raise Http404
|
|
|
|
|
|
|
|
payment = get_eopayment_object(request, transaction.regie)
|
|
|
|
amount = request.GET['amount']
|
|
|
|
|
|
|
|
logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id))
|
|
|
|
try:
|
|
|
|
result = payment.backend.cancel(Decimal(amount), transaction.bank_data)
|
|
|
|
except eopayment.ResponseError as e:
|
|
|
|
logger.error(u'failed in cancel operation: %s', e)
|
|
|
|
response = HttpResponse(content_type='application/json')
|
2018-07-25 14:28:05 +02:00
|
|
|
response.write(json.dumps({'err': 1, 'e': force_text(e)}))
|
2016-08-21 12:53:03 +02:00
|
|
|
return response
|
|
|
|
|
|
|
|
logger.info(u'bank cancellation result: %r', result)
|
|
|
|
operation = TransactionOperation(transaction=transaction,
|
|
|
|
kind='cancellation', amount=Decimal(amount), bank_result=result)
|
|
|
|
operation.save()
|
|
|
|
|
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
response.write(json.dumps({'err': 0, 'extra': result}))
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2018-09-24 17:56:36 +02:00
|
|
|
class PayMixin(object):
|
2016-06-13 10:10:30 +02:00
|
|
|
@atomic
|
2018-09-24 17:56:36 +02:00
|
|
|
def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''):
|
|
|
|
if remote_items:
|
|
|
|
total_amount = sum([x.amount for x in remote_items])
|
2017-06-14 15:46:10 +02:00
|
|
|
else:
|
2018-09-24 17:56:36 +02:00
|
|
|
total_amount = sum([x.amount for x in items])
|
2017-06-14 15:46:10 +02:00
|
|
|
|
2018-09-24 17:56:36 +02:00
|
|
|
if total_amount < regie.payment_min_amount:
|
|
|
|
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
|
|
|
|
return HttpResponseRedirect(next_url)
|
2015-03-05 17:02:52 +01:00
|
|
|
|
2018-09-24 17:56:36 +02:00
|
|
|
for item in items:
|
|
|
|
if item.regie != regie:
|
|
|
|
messages.error(request, _(u'Invalid grouping for basket items.'))
|
|
|
|
return HttpResponseRedirect(next_url)
|
|
|
|
user = request.user if request.user.is_authenticated() else None
|
2015-03-05 17:02:52 +01:00
|
|
|
transaction = Transaction()
|
2017-11-15 08:19:09 +01:00
|
|
|
if user:
|
|
|
|
transaction.user = user
|
|
|
|
email = user.email
|
|
|
|
firstname = user.first_name
|
|
|
|
lastname = user.last_name
|
2015-09-17 16:20:38 +02:00
|
|
|
else:
|
|
|
|
transaction.user = None
|
2016-07-22 18:06:45 +02:00
|
|
|
firstname = ''
|
|
|
|
lastname = ''
|
|
|
|
|
2015-03-05 17:02:52 +01:00
|
|
|
transaction.save()
|
2015-09-17 16:20:38 +02:00
|
|
|
transaction.regie = regie
|
2015-03-05 17:02:52 +01:00
|
|
|
transaction.items = items
|
2017-06-14 15:46:10 +02:00
|
|
|
transaction.remote_items = ','.join([x.id for x in remote_items])
|
2015-04-21 16:40:46 +02:00
|
|
|
transaction.status = 0
|
2015-12-08 15:30:41 +01:00
|
|
|
transaction.amount = total_amount
|
2015-10-05 18:03:52 +02:00
|
|
|
|
2016-02-16 19:43:14 +01:00
|
|
|
payment = get_eopayment_object(request, regie)
|
2016-07-22 18:06:45 +02:00
|
|
|
|
|
|
|
(order_id, kind, data) = payment.request(total_amount, email=email,
|
|
|
|
first_name=firstname,
|
|
|
|
last_name=lastname)
|
2016-06-13 10:52:19 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.info(u'emitted payment request with id %s', smart_text(order_id), extra={
|
|
|
|
'eopayment_order_id': smart_text(order_id), 'eopayment_data': repr(data)})
|
2015-03-05 17:02:52 +01:00
|
|
|
transaction.order_id = order_id
|
|
|
|
transaction.save()
|
|
|
|
|
2018-09-24 17:56:36 +02:00
|
|
|
# store the next url in session in order to be able to redirect to
|
|
|
|
# it if payment is canceled
|
|
|
|
request.session.setdefault('lingo_next_url',
|
|
|
|
{})[order_id] = request.build_absolute_uri(next_url)
|
|
|
|
request.session.modified = True
|
2015-03-05 17:02:52 +01:00
|
|
|
|
|
|
|
if kind == eopayment.URL:
|
|
|
|
return HttpResponseRedirect(data)
|
2015-03-06 10:47:34 +01:00
|
|
|
elif kind == eopayment.FORM:
|
|
|
|
return TemplateResponse(request, 'lingo/payment_form.html', {'form': data})
|
2015-03-05 17:02:52 +01:00
|
|
|
|
|
|
|
raise NotImplementedError()
|
2015-03-06 10:47:34 +01:00
|
|
|
|
|
|
|
|
2018-09-24 17:56:36 +02:00
|
|
|
class PayView(PayMixin, View):
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
regie_id = request.POST.get('regie')
|
|
|
|
next_url = request.POST.get('next_url') or '/'
|
|
|
|
user = request.user if request.user.is_authenticated() else None
|
|
|
|
remote_items = []
|
|
|
|
items = []
|
|
|
|
if regie_id and Regie.objects.get(pk=regie_id).is_remote():
|
|
|
|
regie = Regie.objects.get(pk=regie_id)
|
|
|
|
# get all items data from regie webservice
|
|
|
|
for item_id in request.POST.getlist('item'):
|
|
|
|
remote_items.append(regie.get_invoice(user, item_id))
|
|
|
|
else:
|
|
|
|
if user is None:
|
|
|
|
messages.error(request, _(u'Payment requires to be logged in.'))
|
|
|
|
return HttpResponseRedirect(next_url)
|
|
|
|
|
|
|
|
if not regie_id:
|
|
|
|
# take all items but check they're from the same regie
|
|
|
|
items = BasketItem.get_items_to_be_paid(user=user)
|
|
|
|
regie_id = items[0].regie_id
|
|
|
|
for item in items:
|
|
|
|
if item.regie_id != regie_id:
|
|
|
|
messages.error(request, _(u'Invalid grouping for basket items.'))
|
|
|
|
return HttpResponseRedirect(next_url)
|
|
|
|
|
|
|
|
regie = Regie.objects.get(id=regie_id)
|
|
|
|
regie.compute_extra_fees(user=user)
|
|
|
|
items = BasketItem.get_items_to_be_paid(user=user).filter(regie=regie)
|
|
|
|
|
|
|
|
if not user and not request.POST.get('email'):
|
|
|
|
messages.warning(request, _(u'You must give an email address.'))
|
|
|
|
return HttpResponseRedirect(request.POST.get('item_url'))
|
|
|
|
email = request.POST.get('email')
|
|
|
|
|
|
|
|
# XXX: mark basket items as being processed (?)
|
|
|
|
return self.handle_payment(request, regie, items, remote_items, next_url, email)
|
|
|
|
|
|
|
|
|
2018-02-19 13:54:48 +01:00
|
|
|
class PaymentException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class UnsignedPaymentException(PaymentException):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownPaymentException(PaymentException):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class PaymentView(View):
|
|
|
|
def handle_response(self, request, backend_response, **kwargs):
|
2015-03-06 10:47:34 +01:00
|
|
|
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
|
2016-02-16 19:43:14 +01:00
|
|
|
payment = get_eopayment_object(request, regie)
|
2016-06-13 10:52:19 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
2018-02-21 09:16:33 +01:00
|
|
|
logger.info(u'received payment response: %r', backend_response)
|
2016-05-06 15:20:16 +02:00
|
|
|
try:
|
|
|
|
payment_response = payment.response(backend_response)
|
|
|
|
except eopayment.ResponseError as e:
|
2016-06-13 10:52:19 +02:00
|
|
|
logger.error(u'failed to process payment response: %s', e,
|
|
|
|
extra={'eopayment_raw_response': repr(backend_response)})
|
2018-02-19 13:54:48 +01:00
|
|
|
raise PaymentException('Failed to process payment response')
|
|
|
|
|
2016-06-13 10:52:19 +02:00
|
|
|
extra_info = {
|
|
|
|
'eopayment_order_id': smart_text(payment_response.order_id),
|
|
|
|
'eopayment_response': repr(payment_response),
|
|
|
|
}
|
2018-07-25 14:28:05 +02:00
|
|
|
for k, v in payment_response.bank_data.items():
|
2016-06-13 10:52:19 +02:00
|
|
|
extra_info['eopayment_bank_data_' + k] = smart_text(v)
|
2017-11-07 09:25:05 +01:00
|
|
|
if not payment_response.signed and not payment_response.result == eopayment.CANCELLED:
|
|
|
|
# we accept unsigned cancellation requests as some platforms do
|
|
|
|
# that :/
|
2016-06-13 10:52:19 +02:00
|
|
|
logger.warning(u'received unsigned payment response with id %s',
|
|
|
|
smart_text(payment_response.order_id), extra=extra_info)
|
2018-02-19 13:54:48 +01:00
|
|
|
raise UnsignedPaymentException('Received unsigned payment response')
|
2015-04-21 16:40:46 +02:00
|
|
|
|
2016-02-19 11:13:37 +01:00
|
|
|
try:
|
|
|
|
transaction = Transaction.objects.get(order_id=payment_response.order_id)
|
|
|
|
except Transaction.DoesNotExist:
|
2016-06-13 10:52:19 +02:00
|
|
|
logger.warning(u'received unknown payment response with id %s',
|
|
|
|
smart_text(payment_response.order_id), extra=extra_info)
|
2018-02-19 13:54:48 +01:00
|
|
|
raise UnknownPaymentException('Received unknown payment response')
|
2016-06-13 10:52:19 +02:00
|
|
|
else:
|
|
|
|
extra_info['lingo_transaction_id'] = transaction.pk
|
|
|
|
if transaction.user:
|
|
|
|
# let hobo logger filter handle the extraction of user's infos
|
|
|
|
extra_info['user'] = transaction.user
|
2017-11-07 09:42:04 +01:00
|
|
|
logger.info(u'received known payment response with id %s',
|
2016-06-13 10:52:19 +02:00
|
|
|
smart_text(payment_response.order_id), extra=extra_info)
|
|
|
|
|
2018-02-21 09:12:17 +01:00
|
|
|
if transaction.status == payment_response.result:
|
|
|
|
# return early if transaction status didn't change (it means the
|
|
|
|
# payment service sent the response both as server to server and
|
|
|
|
# via the user browser and we already handled one).
|
|
|
|
return transaction
|
|
|
|
|
2017-11-15 07:42:01 +01:00
|
|
|
if transaction.status and transaction.status != payment_response.result:
|
2018-02-21 09:12:17 +01:00
|
|
|
logger.info(u'received payment notification on existing transaction '
|
2017-11-15 07:42:01 +01:00
|
|
|
'(status: %s, new status: %s)' % (
|
|
|
|
transaction.status, payment_response.result))
|
|
|
|
|
2018-02-19 13:54:48 +01:00
|
|
|
# check if transaction belongs to right regie
|
|
|
|
if not transaction.regie == regie:
|
|
|
|
logger.warning(u'received payment for inappropriate regie '
|
|
|
|
'(expecteds: %s, received: %s)' % (transaction.regie, regie))
|
|
|
|
raise PaymentException('Invalid payment regie')
|
|
|
|
|
2015-04-21 16:40:46 +02:00
|
|
|
transaction.status = payment_response.result
|
2016-06-06 19:48:38 +02:00
|
|
|
transaction.bank_transaction_id = payment_response.transaction_id
|
2015-03-06 10:47:34 +01:00
|
|
|
transaction.bank_data = payment_response.bank_data
|
2015-03-07 14:01:24 +01:00
|
|
|
transaction.end_date = timezone.now()
|
2015-03-06 10:47:34 +01:00
|
|
|
transaction.save()
|
2015-04-21 16:40:46 +02:00
|
|
|
|
2018-01-31 15:25:13 +01:00
|
|
|
if payment_response.result == eopayment.WAITING:
|
|
|
|
# mark basket items as waiting for payment confirmation
|
|
|
|
transaction.items.all().update(waiting_date=timezone.now())
|
2018-02-19 13:54:48 +01:00
|
|
|
return transaction
|
2018-01-31 15:25:13 +01:00
|
|
|
|
|
|
|
if payment_response.result == eopayment.CANCELLED:
|
|
|
|
# mark basket items as no longer waiting so the user can restart a
|
|
|
|
# payment.
|
|
|
|
transaction.items.all().update(waiting_date=None)
|
2018-02-19 13:54:48 +01:00
|
|
|
return transaction
|
2018-01-31 15:25:13 +01:00
|
|
|
|
2017-11-15 07:42:01 +01:00
|
|
|
if payment_response.result not in (eopayment.PAID, eopayment.ACCEPTED):
|
2018-02-19 13:54:48 +01:00
|
|
|
return transaction
|
2015-04-21 16:40:46 +02:00
|
|
|
|
2017-09-11 10:49:32 +02:00
|
|
|
transaction.items.update(payment_date=transaction.end_date)
|
|
|
|
|
2015-03-06 10:47:34 +01:00
|
|
|
for item in transaction.items.all():
|
2015-03-07 14:00:17 +01:00
|
|
|
try:
|
2016-03-08 15:35:55 +01:00
|
|
|
item.notify_payment()
|
2017-08-17 08:57:47 +02:00
|
|
|
except:
|
|
|
|
# ignore errors, it will be retried later on if it fails
|
|
|
|
logger.exception('error in sync notification for basket item %s', item.id)
|
2017-05-28 12:47:24 +02:00
|
|
|
regie.compute_extra_fees(user=transaction.user)
|
2015-10-14 18:53:25 +02:00
|
|
|
if transaction.remote_items:
|
2017-01-17 21:05:23 +01:00
|
|
|
transaction.first_notify_remote_items_of_payments()
|
2018-02-19 13:54:48 +01:00
|
|
|
|
|
|
|
return transaction
|
|
|
|
|
|
|
|
|
|
|
|
class CallbackView(PaymentView):
|
|
|
|
def handle_callback(self, request, backend_response, **kwargs):
|
|
|
|
try:
|
|
|
|
self.handle_response(request, backend_response, **kwargs)
|
|
|
|
except UnknownPaymentException as e:
|
2018-07-25 14:28:05 +02:00
|
|
|
raise Http404(force_text(e))
|
2018-02-19 13:54:48 +01:00
|
|
|
except PaymentException as e:
|
2018-07-25 14:28:05 +02:00
|
|
|
return HttpResponseBadRequest(force_text(e))
|
2015-10-13 12:29:53 +02:00
|
|
|
return HttpResponse()
|
|
|
|
|
2015-12-17 09:53:22 +01:00
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
return self.handle_callback(request, request.environ['QUERY_STRING'], **kwargs)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
return self.handle_callback(request, request.body, **kwargs)
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(CallbackView, self).dispatch(*args, **kwargs)
|
|
|
|
|
2015-10-13 12:29:53 +02:00
|
|
|
|
2018-02-19 13:54:48 +01:00
|
|
|
class ReturnView(PaymentView):
|
2016-02-16 19:43:14 +01:00
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(ReturnView, self).dispatch(*args, **kwargs)
|
2015-10-13 12:29:53 +02:00
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
2018-05-09 07:58:12 +02:00
|
|
|
if not request.environ['QUERY_STRING']:
|
|
|
|
return HttpResponseBadRequest('Missing query string')
|
2016-02-16 19:43:14 +01:00
|
|
|
return self.handle_return(request, request.environ['QUERY_STRING'], **kwargs)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
return self.handle_return(request, request.body, **kwargs)
|
|
|
|
|
|
|
|
def handle_return(self, request, backend_response, **kwargs):
|
2018-02-19 13:54:48 +01:00
|
|
|
transaction = None
|
2015-10-13 12:29:53 +02:00
|
|
|
try:
|
2018-02-19 13:54:48 +01:00
|
|
|
transaction = self.handle_response(request, backend_response, **kwargs)
|
|
|
|
except UnsignedPaymentException as e:
|
|
|
|
# some payment backends do not sign return URLs, don't mark this as
|
|
|
|
# an error, they will provide a notification to the callback
|
|
|
|
# endpoint.
|
|
|
|
pass
|
|
|
|
except PaymentException as e:
|
2016-05-26 14:12:32 +02:00
|
|
|
messages.error(request, _('We are sorry but the payment service '
|
|
|
|
'failed to provide a correct answer.'))
|
|
|
|
return HttpResponseRedirect(get_basket_url())
|
2015-10-13 12:29:53 +02:00
|
|
|
|
2018-02-19 13:54:48 +01:00
|
|
|
if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
|
|
|
|
messages.info(request, transaction.regie.get_text_on_success())
|
2015-10-13 12:29:53 +02:00
|
|
|
|
2018-02-19 13:54:48 +01:00
|
|
|
if transaction and request.session.get('lingo_next_url'):
|
2016-03-09 15:17:21 +01:00
|
|
|
redirect_url = request.session['lingo_next_url'].get(transaction.order_id)
|
|
|
|
if redirect_url:
|
|
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
|
|
|
|
# return to basket page if there are still items to pay
|
2016-06-27 15:59:22 +02:00
|
|
|
if request.user.is_authenticated():
|
2017-10-11 17:20:56 +02:00
|
|
|
remaining_basket_items = BasketItem.get_items_to_be_paid(
|
|
|
|
user=self.request.user).count()
|
2016-06-27 15:59:22 +02:00
|
|
|
if remaining_basket_items:
|
|
|
|
return HttpResponseRedirect(get_basket_url())
|
2016-02-03 10:49:47 +01:00
|
|
|
return HttpResponseRedirect('/')
|
2015-09-03 16:03:44 +02:00
|
|
|
|
|
|
|
|
|
|
|
class ItemDownloadView(View):
|
|
|
|
http_method_names = [u'get']
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
2016-07-27 09:09:23 +02:00
|
|
|
try:
|
|
|
|
regie = Regie.objects.get(pk=kwargs['regie_id'])
|
|
|
|
except Regie.DoesNotExist:
|
|
|
|
raise Http404()
|
|
|
|
|
2016-05-24 11:50:22 +02:00
|
|
|
try:
|
2016-07-20 18:20:41 +02:00
|
|
|
item_id = aes_hex_decrypt(settings.SECRET_KEY, kwargs['item_crypto_id'])
|
2016-07-27 09:09:23 +02:00
|
|
|
except DecryptionError:
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
try:
|
2016-10-21 14:06:20 +02:00
|
|
|
data = regie.get_invoice_pdf(request.user, item_id)
|
2016-05-24 11:50:22 +02:00
|
|
|
except PermissionDenied:
|
|
|
|
return HttpResponseForbidden()
|
2016-07-22 18:06:45 +02:00
|
|
|
except DecryptionError as e:
|
|
|
|
return Http404(str(e))
|
2016-05-24 11:50:22 +02:00
|
|
|
|
|
|
|
if data.status_code != 200:
|
|
|
|
logging.error('failed to retrieve invoice (%r)', data.status_code)
|
|
|
|
messages.error(request, _('We are sorry but an error occured when retrieving the invoice.'))
|
|
|
|
if self.request.META.get('HTTP_REFERER'):
|
|
|
|
return HttpResponseRedirect(self.request.META.get('HTTP_REFERER'))
|
|
|
|
return HttpResponseRedirect('/')
|
|
|
|
|
2015-09-03 16:03:44 +02:00
|
|
|
r = HttpResponse(data, content_type='application/pdf')
|
2016-08-24 10:14:20 +02:00
|
|
|
r['Content-Disposition'] = 'attachment; filename="%s.pdf"' % item_id
|
2015-09-03 16:03:44 +02:00
|
|
|
return r
|
2015-09-04 10:41:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
class ItemView(TemplateView):
|
|
|
|
http_method_names = [u'get']
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
2016-07-22 18:06:45 +02:00
|
|
|
ret = {'item_url': self.request.get_full_path()}
|
2016-10-21 14:06:20 +02:00
|
|
|
|
2016-07-27 09:09:23 +02:00
|
|
|
try:
|
|
|
|
regie = Regie.objects.get(pk=kwargs['regie_id'])
|
|
|
|
except Regie.DoesNotExist:
|
|
|
|
raise Http404()
|
2016-07-22 18:06:45 +02:00
|
|
|
|
2016-07-27 09:09:23 +02:00
|
|
|
try:
|
|
|
|
item_id = aes_hex_decrypt(settings.SECRET_KEY, kwargs['item_crypto_id'])
|
|
|
|
except DecryptionError:
|
|
|
|
raise Http404()
|
2016-10-21 14:06:20 +02:00
|
|
|
|
|
|
|
item = regie.get_invoice(self.request.user, item_id)
|
2016-07-20 18:20:41 +02:00
|
|
|
if not item:
|
|
|
|
raise Http404(_('No item was found.'))
|
2018-04-03 14:17:24 +02:00
|
|
|
if self.request.GET.get('page'):
|
|
|
|
try:
|
|
|
|
ret['page'] = Page.objects.get(pk=self.request.GET['page'])
|
|
|
|
except (Page.DoesNotExist, ValueError):
|
|
|
|
pass
|
2016-07-22 18:06:45 +02:00
|
|
|
ret.update({'item': item, 'regie': regie})
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def get_template_names(self):
|
|
|
|
if self.request.is_ajax:
|
|
|
|
return ['lingo/combo/item.html']
|
|
|
|
return ['lingo/combo/invoice_fullpage.html']
|
2016-03-08 15:35:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
class CancelItemView(DetailView):
|
|
|
|
model = BasketItem
|
|
|
|
template_name = 'lingo/combo/cancel-item.html'
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super(CancelItemView, self).get_context_data(**kwargs)
|
|
|
|
context['basket_url'] = get_basket_url()
|
|
|
|
return context
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2017-12-19 11:01:05 +01:00
|
|
|
user = self.request.user if self.request.user.is_authenticated() else None
|
|
|
|
return BasketItem.get_items_to_be_paid(user=user)
|
2016-03-08 15:35:55 +01:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2016-11-26 08:17:21 +01:00
|
|
|
if not request.user.is_authenticated():
|
|
|
|
messages.error(request, _('An error occured when removing the item. '
|
|
|
|
'(no authenticated user)'))
|
|
|
|
return HttpResponseRedirect(get_basket_url())
|
2016-05-31 15:40:31 +02:00
|
|
|
if not self.get_object().user_cancellable:
|
2016-11-26 08:17:21 +01:00
|
|
|
messages.error(request, _('This item cannot be removed.'))
|
2016-05-31 15:40:31 +02:00
|
|
|
return HttpResponseRedirect(get_basket_url())
|
2016-03-08 15:35:55 +01:00
|
|
|
try:
|
2017-04-07 17:44:42 +02:00
|
|
|
self.get_object().notify_cancellation(notify_origin=True)
|
2016-03-08 15:35:55 +01:00
|
|
|
except requests.exceptions.HTTPError:
|
|
|
|
messages.error(request, _('An error occured when removing the item.'))
|
|
|
|
return HttpResponseRedirect(get_basket_url())
|
2016-10-20 10:04:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
class SelfInvoiceView(View):
|
|
|
|
http_method_names = ['get', 'options']
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(SelfInvoiceView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
try:
|
|
|
|
obj = SelfDeclaredInvoicePayment.objects.get(id=kwargs['cell_id'])
|
|
|
|
except SelfDeclaredInvoicePayment.DoesNotExist:
|
|
|
|
raise Http404()
|
|
|
|
invoice_id = request.GET.get('invoice-number', '')
|
|
|
|
invoice_amount = request.GET.get('invoice-amount', '').replace(',', '.')
|
|
|
|
msg = None
|
|
|
|
url = None
|
|
|
|
try:
|
|
|
|
invoice_amount = Decimal(invoice_amount)
|
|
|
|
except ArithmeticError:
|
|
|
|
invoice_amount = '-'
|
|
|
|
msg = _('Sorry, the provided amount is invalid.')
|
|
|
|
else:
|
|
|
|
for regie in obj.get_regies():
|
|
|
|
try:
|
2017-04-11 15:52:27 +02:00
|
|
|
invoice = regie.get_invoice(None, invoice_id, log_errors=False)
|
2016-10-20 10:04:59 +02:00
|
|
|
except ObjectDoesNotExist:
|
|
|
|
continue
|
|
|
|
if invoice.total_amount != invoice_amount:
|
|
|
|
continue
|
|
|
|
url = reverse('view-item', kwargs={
|
|
|
|
'regie_id': regie.id, 'item_crypto_id': invoice.crypto_id})
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
msg = _('Sorry, no invoice were found with that number and amount.')
|
|
|
|
if request.GET.get('ajax') == 'on':
|
|
|
|
response = HttpResponse(content_type='application/json')
|
|
|
|
response.write(json.dumps({'url': url, 'msg': msg and force_text(msg)}))
|
|
|
|
return response
|
|
|
|
if url:
|
|
|
|
return HttpResponseRedirect(url)
|
|
|
|
messages.warning(request, msg)
|
|
|
|
return HttpResponseRedirect(request.GET.get('page_path') or '/')
|