combo/combo/apps/lingo/views.py

1010 lines
39 KiB
Python

# 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/>.
import json
import logging
from decimal import ROUND_HALF_UP, Decimal
import eopayment
import requests
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
from django.core import signing
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db.models.query import Q
from django.db.transaction import atomic
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils import dateparse
from django.utils.encoding import force_str, smart_text
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, ListView, TemplateView, View
from combo.data.models import Page
from combo.profile.utils import get_user_from_name_id
from combo.public.views import publish_page
from combo.utils import DecryptionError, aes_hex_decrypt, check_request_signature
from .models import (
EXPIRED,
BasketItem,
LingoBasketCell,
LingoException,
PaymentBackend,
Regie,
RemoteInvoiceException,
RemoteItem,
SelfDeclaredInvoicePayment,
Transaction,
TransactionOperation,
UnsignedPaymentException,
)
from .utils import signing_dumps, signing_loads
logger = logging.getLogger(__name__)
class EmptyPaymentResponse(LingoException):
pass
class ErrorJsonResponse(JsonResponse):
def __init__(self, err_desc, *args, **kwargs):
data = {'err': 1, 'err_desc': err_desc}
super().__init__(data, *args, **kwargs)
class BadRequestJsonResponse(ErrorJsonResponse):
status_code = 400
def get_basket_url():
basket_cell = LingoBasketCell.objects.filter(page__snapshot__isnull=True).first()
if basket_cell:
return basket_cell.page.get_online_url()
return '/'
def lingo_check_request_signature(request):
keys = []
if getattr(settings, 'LINGO_API_SIGN_KEY', None):
keys = [settings.LINGO_API_SIGN_KEY]
return check_request_signature(request, keys=keys)
class LocaleDecimal(Decimal):
# accept , instead of . for French users comfort
def __new__(cls, value="0", *args, **kwargs):
if isinstance(value, str) and settings.LANGUAGE_CODE.startswith('fr-'):
value = value.replace(',', '.')
return super().__new__(cls, value, *args, **kwargs)
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
class AddBasketItemApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_amount(self, amount):
if isinstance(amount, list):
d = Decimal(sum(LocaleDecimal(a) for a in amount))
else:
d = LocaleDecimal(amount)
return d.quantize(Decimal('0.01'), ROUND_HALF_UP)
def post(self, request, *args, **kwargs):
if not lingo_check_request_signature(request):
return HttpResponseForbidden()
try:
request_body = json.loads(force_str(request.body))
except json.JSONDecodeError:
return BadRequestJsonResponse('bad json request: "%s"' % request.body)
extra = request_body.get('extra', {})
if 'amount' not in request.GET and not 'amount' in request_body and not 'amount' in extra:
return BadRequestJsonResponse('missing amount parameter')
if 'display_name' not in request_body:
return HttpResponseBadRequest('missing display_name parameter')
item = BasketItem(amount=0)
try:
item.amount = self.get_amount(request.GET.getlist('amount'))
except ArithmeticError:
return BadRequestJsonResponse('invalid value for "amount" in query string')
if request_body.get('amount'):
try:
item.amount += self.get_amount(request_body['amount'])
except ArithmeticError:
return BadRequestJsonResponse('invalid value for "amount" in payload')
if extra.get('amount'):
try:
item.amount += self.get_amount(extra['amount'])
except ArithmeticError:
return BadRequestJsonResponse('invalid value for "amount" in extra payload')
if 'extra' in request_body:
item.request_data = request_body.get('extra')
else:
item.request_data = request_body
try:
if request.GET.get('NameId'):
user = get_user_from_name_id(request.GET.get('NameId'), raise_on_missing=True)
else:
user = None
item.email = request_body.get('email') or ''
except User.DoesNotExist:
return BadRequestJsonResponse('unknown user')
item.user = user
if request.GET.get('regie_id'):
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 BadRequestJsonResponse('unknown regie')
else:
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]
if item.regie.is_remote():
return BadRequestJsonResponse('can not add a basket item to a remote regie')
if request.GET.get('cancellable') == 'no':
item.user_cancellable = False
item.subject = request_body['display_name']
item.source_url = request_body.get('url') or ''
item.reference_id = request_body.get('reference_id') or ''
if 'capture_date' in request_body:
try:
# parse_date returns None when the string format is invalid
capture_date_err = False
item.capture_date = dateparse.parse_date(request_body['capture_date'])
except TypeError:
capture_date_err = True
if item.capture_date is None or capture_date_err:
return BadRequestJsonResponse('bad format for capture date, it should be yyyy-mm-dd')
item.save()
if user:
item.regie.compute_extra_fees(user=item.user)
else:
if item.regie.extra_fees_ws_url:
BadRequestJsonResponse('can not compute extra fees with anonymous user')
return JsonResponse(
{
'result': 'success',
'id': str(item.id),
'payment_url': request.build_absolute_uri(item.payment_url),
}
)
class RemoveBasketItemApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not lingo_check_request_signature(request):
return HttpResponseForbidden()
try:
request_body = json.loads(force_str(request.body))
except json.JSONDecodeError:
return BadRequestJsonResponse('bad json request: "%s"' % request.body)
if 'basket_item_id' not in request_body:
return BadRequestJsonResponse('missing basket_item_id parameter')
try:
item = BasketItem.objects.get(id=request_body.get('basket_item_id'))
except BasketItem.DoesNotExist:
return BadRequestJsonResponse('unknown basket item')
except ValueError:
return BadRequestJsonResponse('invalid basket_item_id')
if item.cancellation_date:
return BadRequestJsonResponse('basket item already cancelled')
try:
if request.GET.get('NameId'):
user = get_user_from_name_id(request.GET.get('NameId'), raise_on_missing=True)
if user is None:
raise User.DoesNotExist()
else:
return BadRequestJsonResponse('no user specified')
except User.DoesNotExist:
return BadRequestJsonResponse('unknown user')
if item.user != user:
return BadRequestJsonResponse('user does not own the basket item')
notify_origin = bool(request_body.get('notify', 'false') == 'true')
item.notify_cancellation(notify_origin=notify_origin)
return JsonResponse({'result': 'success'})
class ValidateTransactionApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not lingo_check_request_signature(request):
return HttpResponseForbidden()
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(
'received validate request for unknown transaction %s', request.GET['transaction_id']
)
raise Http404
amount = LocaleDecimal(request.GET['amount'])
logger.info('validating amount %s for transaction %s', amount, smart_text(transaction.id))
try:
result = transaction.make_eopayment(request=request).backend.validate(
amount, transaction.bank_data
)
except eopayment.ResponseError as e:
logger.error('failed in validation operation: %s', e)
return JsonResponse({'err': 1, 'e': force_str(e)})
logger.info('bank validation result: %r', result)
operation = TransactionOperation(
transaction=transaction, kind='validation', amount=amount, bank_result=result
)
operation.save()
return JsonResponse({'err': 0, 'extra': result})
class CancelTransactionApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not lingo_check_request_signature(request):
return HttpResponseForbidden()
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(
'received validate request for unknown transaction %s', request.GET['transaction_id']
)
raise Http404
amount = LocaleDecimal(request.GET['amount'])
logger.info('cancelling amount %s for transaction %s', amount, smart_text(transaction.id))
try:
result = transaction.make_eopayment(request=request).backend.cancel(amount, transaction.bank_data)
except eopayment.ResponseError as e:
logger.error('failed in cancel operation: %s', e)
return JsonResponse({'err': 1, 'e': force_str(e)})
logger.info('bank cancellation result: %r', result)
operation = TransactionOperation(
transaction=transaction, kind='cancellation', amount=amount, bank_result=result
)
operation.save()
return JsonResponse({'err': 0, 'extra': result})
class PayMixin:
@atomic
def handle_payment(
self, request, regie, items, remote_items, next_url='/', email='', firstname='', lastname=''
):
# check contract
if bool(len(items)) == bool(len(remote_items)):
messages.error(request, _('Items to pay are missing or are not of the same type (local/remote).'))
return HttpResponseRedirect(next_url)
if (
regie.payment_backend.can_poll_backend()
and self.poll_for_newly_paid_or_still_running_transactions(regie, items, remote_items)
):
messages.error(request, _('Some items are already paid or are being paid.'))
return HttpResponseRedirect(next_url)
if regie.can_pay_only_one_basket_item and (len(items) > 1 or len(remote_items) > 1):
messages.error(request, _('This regie allows to pay only one item.'))
return HttpResponseRedirect(next_url)
if any(item.paid for item in remote_items):
messages.error(request, _('Some items are already paid.'))
return HttpResponseRedirect(next_url)
total_amount = sum(x.amount for x in remote_items or items)
if total_amount < regie.payment_min_amount:
messages.warning(request, _('Minimal payment amount is %s €.') % regie.payment_min_amount)
return HttpResponseRedirect(
get_payment_status_view(next_url=next_url if remote_items else items[0].source_url)
)
for item in items:
if item.regie != regie:
messages.error(request, _('Invalid grouping for basket items.'))
return HttpResponseRedirect(next_url)
for item in items:
if not item.is_notifiable():
messages.error(request, _('At least one item is not linked to a payable form.'))
return HttpResponseRedirect(next_url)
user = request.user if request.user.is_authenticated else None
transaction = Transaction()
if user:
transaction.user = user
firstname = user.first_name
lastname = user.last_name
else:
transaction.user = None
transaction.save()
transaction.regie = regie
transaction.items.set(items)
transaction.remote_items = ','.join([x.id for x in remote_items])
transaction.status = 0
transaction.amount = total_amount
kwargs = {'email': email, 'first_name': firstname, 'last_name': lastname}
kwargs['merchant_name'] = settings.TEMPLATE_VARS.get('global_title') or 'Compte Citoyen'
kwargs['items_info'] = []
for item in remote_items or items:
kwargs['items_info'].append(
{
'text': item.subject,
'amount': item.amount,
'reference_id': item.reference_id,
}
)
if items:
capture_date = items[0].capture_date
if capture_date:
kwargs['capture_date'] = capture_date
if regie.can_pay_only_one_basket_item:
item = (items or remote_items)[0]
kwargs['subject'] = item.subject
# copy command reference / invoice number
if item.reference_id:
kwargs['orderid'] = item.reference_id
if getattr(item, 'request_data', None):
# PayFiP/TIPI specific
if regie.payment_backend.service in ('payfip_ws', 'tipi'):
if item.request_data.get('exer') and item.request_data.get('refdet'):
kwargs['exer'] = item.request_data['exer']
kwargs['refdet'] = item.request_data['refdet']
# allow easy testing/use of backend specific keyword arguments
EOPAYMENT_REQUEST_KWARGS_PREFIX = 'eopayment_request_kwargs_'
for key in item.request_data:
if key.startswith(EOPAYMENT_REQUEST_KWARGS_PREFIX):
arg_name = key[len(EOPAYMENT_REQUEST_KWARGS_PREFIX) :]
kwargs[arg_name] = item.request_data[key]
if regie.transaction_options:
kwargs.update(regie.transaction_options)
try:
(order_id, kind, data) = transaction.make_eopayment(request=request).request(
total_amount, **kwargs
)
except eopayment.PaymentException as e:
logger.error('failed to initiate payment request: %s', e)
messages.error(request, _('Failed to initiate payment request'))
return HttpResponseRedirect(get_payment_status_view(next_url=next_url))
logger.info(
'emitted payment request with id %s',
smart_text(order_id),
extra={'eopayment_order_id': smart_text(order_id), 'eopayment_data': repr(data)},
)
transaction.order_id = order_id
transaction.save()
# store the next url in session in order to be able to redirect to
# it if payment is canceled
if next_url:
request.session.setdefault('lingo_next_url', {})[
str(transaction.pk)
] = request.build_absolute_uri(next_url)
request.session.modified = True
if kind == eopayment.URL:
return HttpResponseRedirect(data)
elif kind == eopayment.FORM:
return TemplateResponse(request, 'lingo/payment_form.html', {'form': data})
raise NotImplementedError()
def poll_for_newly_paid_or_still_running_transactions(self, regie, items, remote_items):
'''Verify if any open transaction is not already paid.'''
qs = Transaction.objects.filter(regie=regie, status__in=Transaction.RUNNING_STATUSES)
if items:
transactions = qs.filter(items__in=items)
else:
transactions = RemoteItem.transactions_for_remote_items(qs, remote_items)
newly_paid_or_still_running = False
for transaction in transactions:
transaction.poll_backend()
newly_paid_or_still_running |= transaction.is_paid() or transaction.is_running()
return newly_paid_or_still_running
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():
try:
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, update_paid=True))
except (requests.exceptions.RequestException, RemoteInvoiceException):
messages.error(request, _('Technical error: impossible to retrieve invoices.'))
return HttpResponseRedirect(next_url)
except ObjectDoesNotExist:
messages.error(request, _('No invoice was found.'))
return HttpResponseRedirect(next_url)
else:
if user is None:
messages.error(request, _('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, _('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 regie.can_pay_only_one_basket_item and (len(items) > 1 or len(remote_items) > 1):
messages.error(request, _('Grouping basket items is not allowed.'))
logger.error(
'lingo: regie can only pay one basket item, but handle_payment() received',
extra={'regie': str(regie), 'items': items, 'remote_items': remote_items},
)
return HttpResponseRedirect(next_url)
if items:
capture_date = items[0].capture_date
for item in items:
if item.capture_date != capture_date:
messages.error(request, _('Invalid grouping for basket items: different capture dates.'))
return HttpResponseRedirect(next_url)
if user:
email = user.email
else:
# user is not authenticated, it comes from ItemCell where an email
# can be given in the payment form.
if not request.POST.get('email'):
messages.warning(request, _('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)
def get_payment_status_view(transaction_id=None, next_url=None):
url = reverse('payment-status')
params = []
if transaction_id:
params.append(('transaction-id', signing_dumps(transaction_id)))
if next_url:
params.append(('next', next_url))
return "%s?%s" % (url, urlencode(params))
class BasketItemPayView(PayMixin, View):
def get(self, request, *args, **kwargs):
next_url = request.GET.get('next_url')
firstname = request.GET.get('firstname', '')
lastname = request.GET.get('lastname', '')
item_signature = kwargs.get('item_signature')
try:
item_id = signing_loads(item_signature)
except signing.BadSignature:
return HttpResponseForbidden(_('Invalid payment request.'))
item = BasketItem.objects.get(pk=item_id)
regie = item.regie
if regie.extra_fees_ws_url:
return HttpResponseForbidden(_('No item payment allowed as extra fees set.'))
# Get an email from request or the item
if request.user.is_authenticated:
email = request.user.email
elif request.GET.get('email'):
email = request.GET.get('email', '')
elif item.user and item.user.email:
email = item.user.email
else:
email = item.email
if not next_url:
next_url = item.source_url
return self.handle_payment(
request=request,
regie=regie,
items=[item],
remote_items=[],
next_url=next_url,
email=email,
firstname=firstname,
lastname=lastname,
)
class PaymentView(View):
def handle_response(self, request, backend_response, *, callback=True, transaction=None, **kwargs):
if 'regie_pk' in kwargs:
try:
regie_pk = int(kwargs['regie_pk'])
except ValueError:
regie_pk = None
payment_backend = PaymentBackend.objects.filter(regie__pk=regie_pk).first()
elif 'payment_backend_pk' in kwargs:
payment_backend_slug = kwargs['payment_backend_pk']
try:
payment_backend_pk = int(kwargs['payment_backend_pk'])
except ValueError:
payment_backend_pk = None
payment_backend = PaymentBackend.objects.filter(
Q(pk=payment_backend_pk) | Q(slug=payment_backend_slug)
).first()
else:
payment_backend = None
if not payment_backend:
logger.error('lingo: payment backend not found on callback kwargs=%r', kwargs)
raise Http404("A payment backend or regie primary key or slug must be specified")
payment = payment_backend.make_eopayment(request=request)
if not backend_response and not payment.has_empty_response:
raise EmptyPaymentResponse
logger.info('received payment response: %r', backend_response)
eopayment_response_kwargs = {'redirect': not callback}
if transaction is not None:
eopayment_response_kwargs.update(
{
'order_id_hint': transaction.order_id,
'order_status_hint': transaction.status,
}
)
try:
payment_response = payment.response(backend_response, **eopayment_response_kwargs)
except eopayment.PaymentException as e:
raise LingoException('eopayment exception: %s' % e)
return payment_backend.handle_backend_response(payment_response)
class CallbackView(PaymentView):
def handle_callback(self, request, backend_response, **kwargs):
try:
transaction = self.handle_response(request, backend_response, **kwargs)
except LingoException as e:
if e.transaction:
logger.warning(
'lingo: received synchronous payment notification for '
'%s: failure "%s" with method "%s" and content %r',
e.transaction,
e,
request.method,
backend_response,
)
else:
logger.warning(
'lingo: received synchronous payment notification for '
'an unknown transaction: failure "%s" with method "%s" and content %r',
e,
request.method,
backend_response,
)
return HttpResponseBadRequest(force_str(e))
logger.info('lingo: received synchronous payment notification for %s', transaction)
return HttpResponse()
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, force_str(request.body), **kwargs)
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
class ReturnView(PaymentView):
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
return self.handle_return(request, request.environ['QUERY_STRING'], **kwargs)
def post(self, request, *args, **kwargs):
return self.handle_return(
request, force_str(request.body) or request.environ['QUERY_STRING'], **kwargs
)
def handle_return(self, request, backend_response, **kwargs):
transaction = None
transaction_id = kwargs.get('transaction_signature')
if transaction_id:
try:
transaction_id = signing_loads(transaction_id)
except signing.BadSignature:
transaction_id = None
else:
transaction = Transaction.objects.filter(id=transaction_id).first()
response_is_ok = True
exception = None
try:
transaction = self.handle_response(
request, backend_response, callback=False, transaction=transaction, **kwargs
)
except EmptyPaymentResponse:
response_is_ok = False
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.
response_is_ok = False
exception = e
except LingoException as e:
response_is_ok = False
exception = e
messages.error(
request, _('We are sorry but the payment service failed to provide a correct answer.')
)
if exception:
transaction = transaction or exception.transaction
if transaction:
logger.warning(
'lingo: error on asynchronous payment notification for '
'%s: failure "%s" with method "%s" and content %r',
transaction,
exception,
request.method,
backend_response,
)
else:
logger.warning(
'lingo: error on asynchronous payment notification for '
'an unknown transaction: failure "%s" with method "%s" and content %r',
exception,
request.method,
backend_response,
)
if not response_is_ok:
if transaction_id:
return HttpResponseRedirect(get_payment_status_view(transaction_id))
return HttpResponseRedirect(get_basket_url())
logger.info('lingo: received asynchronous payment notification for %s', transaction)
if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
messages.info(request, transaction.regie.get_text_on_success())
if transaction:
return HttpResponseRedirect(get_payment_status_view(transaction.pk))
# return to basket page if there are still items to pay
if request.user.is_authenticated:
remaining_basket_items = BasketItem.get_items_to_be_paid(user=self.request.user).count()
if remaining_basket_items:
return HttpResponseRedirect(get_basket_url())
return HttpResponseRedirect('/')
class ItemDownloadView(View):
http_method_names = ['get']
def get(self, request, *args, **kwargs):
try:
regie = Regie.objects.get(pk=kwargs['regie_id'])
except Regie.DoesNotExist:
raise Http404()
try:
item_id = aes_hex_decrypt(settings.SECRET_KEY, kwargs['item_crypto_id'])
except DecryptionError:
raise Http404()
try:
data = regie.get_invoice_pdf(request.user, item_id)
except PermissionDenied:
return HttpResponseForbidden()
except DecryptionError as e:
return Http404(str(e))
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('/')
r = HttpResponse(data, content_type='application/pdf')
r['Content-Disposition'] = 'attachment; filename="%s.pdf"' % item_id
return r
class ItemView(TemplateView):
http_method_names = ['get']
def get_context_data(self, **kwargs):
ret = {'item_url': self.request.get_full_path()}
regie = get_object_or_404(Regie, pk=kwargs['regie_id'])
try:
item_id = aes_hex_decrypt(settings.SECRET_KEY, kwargs['item_crypto_id'])
except DecryptionError:
raise Http404()
try:
item = regie.get_invoice(self.request.user, item_id, update_paid=True)
if self.request.GET.get('page'):
try:
ret['page'] = Page.objects.get(pk=self.request.GET['page'])
except (Page.DoesNotExist, ValueError):
pass
ret.update({'item': item, 'regie': regie})
return ret
except (requests.exceptions.RequestException, RemoteInvoiceException):
return {'item': None, 'err_desc': _('Technical error: impossible to retrieve invoices.')}
except ObjectDoesNotExist:
return {'item': None, 'err_desc': _('No item was found.')}
def get_template_names(self):
if self.request.is_ajax:
return ['lingo/combo/item.html']
return ['lingo/combo/invoice_fullpage.html']
class CancelItemView(DetailView):
model = BasketItem
template_name = 'lingo/combo/cancel-item.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['basket_url'] = get_basket_url()
return context
def get_queryset(self):
user = self.request.user if self.request.user.is_authenticated else None
return BasketItem.get_items_to_be_paid(user=user)
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.error(request, _('An error occured when removing the item. (no authenticated user)'))
return HttpResponseRedirect(get_basket_url())
if not self.get_object().user_cancellable:
messages.error(request, _('This item cannot be removed.'))
return HttpResponseRedirect(get_basket_url())
try:
self.get_object().notify_cancellation(notify_origin=True)
except requests.exceptions.HTTPError:
messages.error(request, _('An error occured when removing the item.'))
return HttpResponseRedirect(get_basket_url())
class SelfInvoiceView(View):
http_method_names = ['get', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super().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', '')
msg = None
url = None
try:
invoice_amount = LocaleDecimal(invoice_amount)
except ArithmeticError:
invoice_amount = '-'
msg = _('Sorry, the provided amount is invalid.')
else:
for regie in obj.get_regies():
try:
invoice = regie.get_invoice(None, invoice_id, log_errors=False, update_paid=True)
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':
return JsonResponse({'url': url, 'msg': msg and force_str(msg)})
if url:
return HttpResponseRedirect(url)
messages.warning(request, msg)
return HttpResponseRedirect(request.GET.get('page_path') or '/')
class PaymentStatusView(View):
http_method_names = ['get']
def get(self, request, *args, **kwargs):
page = Page()
page.template_name = 'standard'
template_name = 'lingo/combo/payment-status.html'
extra_context_data = getattr(request, 'extra_context_data', {})
extra_context_data['transaction_id'] = ''
transaction_id = request.GET.get('transaction-id')
if not transaction_id:
next_url = request.GET.get('next')
if not next_url:
next_url = '/'
extra_context_data['next_url'] = request.build_absolute_uri(next_url)
request.extra_context_data = extra_context_data
return publish_page(request, page, template_name=template_name)
try:
transaction_id = signing_loads(transaction_id)
except signing.BadSignature:
return HttpResponseForbidden(_('Invalid transaction signature.'))
try:
transaction = Transaction.objects.get(pk=transaction_id)
except Transaction.DoesNotExist:
return HttpResponseForbidden(_('Invalid transaction.'))
next_url = request.session.get('lingo_next_url', {}).get(str(transaction_id))
if not next_url:
next_url = get_basket_url()
if len({item.source_url for item in transaction.items.all()}) == 1:
next_url = transaction.items.first().source_url
next_url = request.build_absolute_uri(next_url)
extra_context_data['transaction_id'] = signing_dumps(transaction.pk)
extra_context_data['next_url'] = next_url
request.extra_context_data = extra_context_data
return publish_page(request, page, template_name=template_name)
class TransactionStatusApiView(View):
http_method_names = ['get']
def get(self, request, *args, **kwargs):
transaction_signature = kwargs.get('transaction_signature')
try:
transaction_id = signing_loads(transaction_signature)
except signing.BadSignature:
return HttpResponseBadRequest(_('Invalid transaction.'))
try:
transaction = Transaction.objects.get(pk=transaction_id)
except (Transaction.DoesNotExist, ValueError):
return HttpResponseNotFound(_('Unknown transaction.'))
user = request.user if request.user.is_authenticated else None
error_msg = _('Transaction does not belong to the requesting user')
if user and transaction.user and user != transaction.user:
return HttpResponseForbidden(error_msg)
if not user and transaction.user:
return HttpResponseForbidden(error_msg)
if transaction.is_paid():
data = {'wait': False, 'error': False, 'error_msg': ''}
return JsonResponse(data=data)
if transaction.status in (eopayment.ERROR, eopayment.DENIED, EXPIRED):
data = {
'wait': True,
'error': True,
'error_msg': _('Payment error, you can continue and make another payment'),
}
return JsonResponse(data=data)
if transaction.status == eopayment.CANCELLED:
data = {
'wait': True,
'error': False,
'error_msg': _('Payment cancelled, you can continue and make another payment'),
}
return JsonResponse(data=data)
data = {'wait': True, 'error': False, 'error_msg': ''}
return JsonResponse(data=data)