# -*- coding: utf-8 -*- # 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 . from decimal import Decimal, ROUND_HALF_UP import json import logging import requests from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.urls import reverse from django.core import signing from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseBadRequest from django.http import HttpResponseForbidden, Http404, JsonResponse from django.template.response import TemplateResponse from django.utils import timezone, dateparse, six from django.utils.encoding import force_text from django.utils.http import urlencode from django.views.decorators.csrf import csrf_exempt from django.views.generic import View, DetailView, ListView, TemplateView from django.conf import settings from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from django.db.transaction import atomic from django.shortcuts import get_object_or_404 from django.utils.encoding import smart_text import eopayment from combo.data.models import Page from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError from combo.profile.utils import get_user_from_name_id from combo.public.views import publish_page from .models import (Regie, BasketItem, Transaction, TransactionOperation, LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED) 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 signing_dumps(content): serialization = signing.dumps(content) return serialization.replace(':', '.') def signing_loads(serialization): serialization = serialization.replace('.', ':') return signing.loads(serialization) def get_eopayment_object(request, regie_or_payment_backend, transaction_id=None): payment_backend = regie_or_payment_backend if isinstance(regie_or_payment_backend, Regie): payment_backend = regie_or_payment_backend.payment_backend options = payment_backend.service_options options.update({ 'automatic_return_url': request.build_absolute_uri( reverse('lingo-callback-payment-backend', kwargs={'payment_backend_pk': payment_backend.id})), }) if transaction_id: options['normal_return_url'] = request.build_absolute_uri( reverse('lingo-return-payment-backend', kwargs={ 'payment_backend_pk': payment_backend.id, 'transaction_signature': signing_dumps(transaction_id) }) ) return eopayment.Payment(payment_backend.service, options) 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, six.string_types) and settings.LANGUAGE_CODE.startswith('fr-'): value = value.replace(',', '.') return super(LocaleDecimal, cls).__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(AddBasketItemApiView, self).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() request_body = json.loads(force_text(self.request.body)) extra = request_body.get('extra', {}) if not 'amount' 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') payment_url = reverse( 'basket-item-pay-view', kwargs={ 'item_signature': signing_dumps(item.pk) }) return JsonResponse({'result': 'success', 'id': str(item.id), 'payment_url': request.build_absolute_uri(payment_url)}) 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): if not lingo_check_request_signature(request): return HttpResponseForbidden() request_body = json.loads(force_text(self.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(ValidateTransactionApiView, self).dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): if not lingo_check_request_signature(request): 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 = LocaleDecimal(request.GET['amount']) logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id)) try: result = payment.backend.validate(amount, transaction.bank_data) except eopayment.ResponseError as e: logger.error(u'failed in validation operation: %s', e) return JsonResponse({'err': 1, 'e': force_text(e)}) logger.info(u'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(CancelTransactionApiView, self).dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): if not lingo_check_request_signature(request): 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 = LocaleDecimal(request.GET['amount']) logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id)) try: result = payment.backend.cancel(amount, transaction.bank_data) except eopayment.ResponseError as e: logger.error(u'failed in cancel operation: %s', e) return JsonResponse({'err': 1, 'e': force_text(e)}) logger.info(u'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(object): @atomic def handle_payment( self, request, regie, items, remote_items, next_url='/', email='', firstname='', lastname=''): if remote_items: total_amount = sum([x.amount for x in remote_items]) else: total_amount = sum([x.amount for x in items]) if total_amount < regie.payment_min_amount: messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount) return HttpResponseRedirect(get_payment_status_view(next_url=items[0].source_url)) 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 transaction = Transaction() if user: transaction.user = user email = user.email 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 payment = get_eopayment_object(request, regie, transaction.pk) 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 not email and item.email: kwargs['email'] = item.email if items: capture_date = items[0].capture_date if capture_date: kwargs['capture_date'] = capture_date if regie.transaction_options: kwargs.update(regie.transaction_options) logger = logging.getLogger(__name__) try: (order_id, kind, data) = payment.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(u'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() 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 items: capture_date = items[0].capture_date for item in items: if item.capture_date != capture_date: messages.error( request, _(u'Invalid grouping for basket items: different capture dates.')) return HttpResponseRedirect(next_url) 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) 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') email = request.GET.get('email', '') 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.')) 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 PaymentException(Exception): pass class UnsignedPaymentException(PaymentException): def __init__(self, transaction, *args, **kwargs): super(UnsignedPaymentException, self).__init__(*args, **kwargs) self.transaction = transaction class UnknownPaymentException(PaymentException): pass class PaymentView(View): def handle_response(self, request, backend_response, **kwargs): if 'regie_pk' in kwargs: payment_backend = get_object_or_404( Regie, pk=kwargs['regie_pk'] ).payment_backend elif 'payment_backend_pk' in kwargs: payment_backend = get_object_or_404( PaymentBackend, pk=kwargs['payment_backend_pk']) else: return HttpResponseBadRequest("A payment backend or regie primary key must be specified") payment = get_eopayment_object(request, payment_backend) logger = logging.getLogger(__name__) logger.info(u'received payment response: %r', backend_response) extra_info = kwargs.pop('payment_extra_info', {}) try: payment_response = payment.response(backend_response, **extra_info) except eopayment.PaymentException as e: logger.error(u'failed to process payment response: %s', e, extra={'eopayment_raw_response': repr(backend_response)}) raise PaymentException('Failed to process payment response') extra_info = { 'eopayment_order_id': smart_text(payment_response.order_id), 'eopayment_response': repr(payment_response), } for k, v in payment_response.bank_data.items(): extra_info['eopayment_bank_data_' + k] = smart_text(v) try: transaction = Transaction.objects.get(order_id=payment_response.order_id) except Transaction.DoesNotExist: logger.warning(u'received unknown payment response with id %s', smart_text(payment_response.order_id), extra=extra_info) raise UnknownPaymentException('Received unknown payment response') 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 logger.info(u'received known payment response with id %s', smart_text(payment_response.order_id), extra=extra_info) 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 if transaction.status and transaction.status != payment_response.result: logger.info(u'received payment notification on existing transaction ' '(status: %s, new status: %s)' % ( transaction.status, payment_response.result)) # check if transaction belongs to right regie if not transaction.regie.payment_backend == payment_backend: logger.warning(u'received payment for inappropriate payment backend ' '(expecteds: %s, received: %s)' % ( transaction.regie.payment_backend, payment_backend)) raise PaymentException('Invalid payment regie') if not payment_response.signed and not payment_response.result == eopayment.CANCELLED: # we accept unsigned cancellation requests as some platforms do # that :/ logger.warning(u'received unsigned payment response with id %s', smart_text(payment_response.order_id), extra=extra_info) raise UnsignedPaymentException(transaction, 'Received unsigned payment response') transaction.status = payment_response.result transaction.bank_transaction_id = payment_response.transaction_id transaction.bank_data = payment_response.bank_data transaction.end_date = timezone.now() # store transaction_date but prevent multiple updates if payment_response.transaction_date is None: logger.warning('no transaction date') elif transaction.bank_transaction_date is None: transaction.bank_transaction_date = payment_response.transaction_date elif payment_response.transaction_date != transaction.bank_transaction_date: # XXX: don't know if it can happen, but I would like to know when it does # as for differed payments there can be multiple notifications. logger.error('new transaction_date for transaction %s was %s, received %s', transaction.id, transaction.bank_transaction_date, payment_response.transaction_date) transaction.save() if payment_response.result == eopayment.WAITING: # mark basket items as waiting for payment confirmation transaction.items.all().update(waiting_date=timezone.now()) return transaction 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) return transaction if payment_response.result not in (eopayment.PAID, eopayment.ACCEPTED): return transaction transaction.items.update(payment_date=transaction.end_date) for item in transaction.items.all(): try: item.notify_payment() except: # ignore errors, it will be retried later on if it fails logger.exception('error in sync notification for basket item %s', item.id) if transaction.remote_items: transaction.first_notify_remote_items_of_payments() 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: raise Http404(force_text(e)) except PaymentException as e: return HttpResponseBadRequest(force_text(e)) 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_text(request.body), **kwargs) @csrf_exempt def dispatch(self, *args, **kwargs): return super(CallbackView, self).dispatch(*args, **kwargs) class ReturnView(PaymentView): @csrf_exempt def dispatch(self, *args, **kwargs): return super(ReturnView, self).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_text(request.body) or request.environ['QUERY_STRING'], **kwargs) def handle_return(self, request, backend_response, **kwargs): payment_extra_info = {'redirect': True} transaction = None transaction_id = kwargs.get('transaction_signature') if transaction_id: try: transaction_id = signing_loads(transaction_id) except signing.BadSignature: transaction_id = None if transaction_id: # retrieve info about previously known state try: current_transaction = Transaction.objects.get(pk=transaction_id) except Transaction.DoesNotExist: pass else: payment_extra_info['order_id_hint'] = current_transaction.order_id payment_extra_info['order_status_hint'] = current_transaction.status try: transaction = self.handle_response( request, backend_response, payment_extra_info=payment_extra_info, **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. if transaction_id: return HttpResponseRedirect(get_payment_status_view(transaction_id)) return HttpResponseRedirect(get_basket_url()) except PaymentException as e: messages.error(request, _('We are sorry but the payment service ' 'failed to provide a correct answer.')) if transaction_id: return HttpResponseRedirect(get_payment_status_view(transaction_id)) return HttpResponseRedirect(get_basket_url()) 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 = [u'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 = [u'get'] def get_context_data(self, **kwargs): ret = {'item_url': self.request.get_full_path()} 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() item = regie.get_invoice(self.request.user, item_id) if not item: raise Http404(_('No item was found.')) 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 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(CancelItemView, self).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(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', '') 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) 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_text(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(set([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.CANCELLED, 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) data = { 'wait': True, 'error': False, 'error_msg': '' } return JsonResponse(data=data)