combo/combo/apps/lingo/views.py

946 lines
36 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.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, six, timezone
from django.utils.encoding import force_text, smart_text
from django.utils.http import urlencode
from django.utils.translation import ugettext_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,
PaymentBackend,
PaymentException,
Regie,
RemoteInvoiceException,
SelfDeclaredInvoicePayment,
Transaction,
TransactionOperation,
UnknownPaymentException,
UnsignedPaymentException,
)
from .utils import signing_dumps, signing_loads
logger = logging.getLogger(__name__)
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_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()
try:
request_body = json.loads(force_text(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(RemoveBasketItemApiView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not lingo_check_request_signature(request):
return HttpResponseForbidden()
try:
request_body = json.loads(force_text(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(ValidateTransactionApiView, self).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(
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()
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=''
):
# 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.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, _(u'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, _(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
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 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) = 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():
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, _(u'Technical error: impossible to retrieve invoices.'))
return HttpResponseRedirect(next_url)
except ObjectDoesNotExist:
messages.error(request, _(u'No invoice was found.'))
return HttpResponseRedirect(next_url)
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 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, _(u'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, _(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')
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:
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.info(u'received payment response: %r', backend_response)
try:
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,
}
)
payment_response = payment.response(backend_response, **eopayment_response_kwargs)
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')
return payment_backend.handle_backend_response(payment_response)
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):
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()
try:
transaction = self.handle_response(
request, backend_response, callback=False, transaction=transaction, **kwargs
)
except UnsignedPaymentException:
# 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:
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()}
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(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, 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_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.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)