Sith/eboutic/views.py
2024-06-26 19:17:57 +02:00

241 lines
9.4 KiB
Python

# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
import base64
import json
from datetime import datetime
from urllib.parse import unquote
import sentry_sdk
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, transaction
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import TemplateView, View
from OpenSSL import crypto
from counter.forms import BillingInfoForm
from counter.models import Counter, Customer, Product
from eboutic.forms import BasketForm
from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products
@login_required
@require_GET
def eboutic_main(request: HttpRequest) -> HttpResponse:
"""
Main view of the eboutic application.
Return an Http response whose content is of type text/html.
The latter represents the page from which a user can see
the catalogue of products that he can buy and fill
his shopping cart.
The purchasable products are those of the eboutic which
belong to a category of products of a product category
(orphan products are inaccessible).
If the session contains a key-value pair that associates "errors"
with a list of strings, this pair is removed from the session
and its value displayed to the user when the page is rendered.
"""
errors = request.session.pop("errors", None)
products = get_eboutic_products(request.user)
context = {
"errors": errors,
"products": products,
"customer_amount": request.user.account_balance,
}
return render(request, "eboutic/eboutic_main.jinja", context)
@require_GET
@login_required
def payment_result(request, result: str) -> HttpResponse:
context = {"success": result == "success"}
return render(request, "eboutic/eboutic_payment_result.jinja", context)
class EbouticCommand(TemplateView):
template_name = "eboutic/eboutic_makecommand.jinja"
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
return redirect("eboutic:main")
@method_decorator(login_required)
def get(self, request: HttpRequest, *args, **kwargs):
form = BasketForm(request)
if not form.is_valid():
request.session["errors"] = form.get_error_messages()
request.session.modified = True
res = redirect("eboutic:main")
res.set_cookie("basket_items", form.get_cleaned_cookie(), path="/eboutic")
return res
basket = Basket.from_session(request.session)
if basket is not None:
basket.clear()
else:
basket = Basket.objects.create(user=request.user)
request.session["basket_id"] = basket.id
request.session.modified = True
items = json.loads(unquote(request.COOKIES["basket_items"]))
items.sort(key=lambda item: item["id"])
ids = [item["id"] for item in items]
quantities = [item["quantity"] for item in items]
products = Product.objects.filter(id__in=ids)
for product, qty in zip(products, quantities):
basket.add_product(product, qty)
kwargs["basket"] = basket
return self.render_to_response(self.get_context_data(**kwargs))
def get_context_data(self, **kwargs):
# basket is already in kwargs when the method is called
default_billing_info = None
if hasattr(self.request.user, "customer"):
customer = self.request.user.customer
kwargs["customer_amount"] = customer.amount
if hasattr(customer, "billing_infos"):
default_billing_info = customer.billing_infos
else:
kwargs["customer_amount"] = None
kwargs["must_fill_billing_infos"] = default_billing_info is None
if not kwargs["must_fill_billing_infos"]:
# the user has already filled its billing_infos, thus we can
# get it without expecting an error
data = kwargs["basket"].get_e_transaction_data()
data = {"data": [{"key": key, "value": val} for key, val in data]}
kwargs["billing_infos"] = json.dumps(data)
kwargs["billing_form"] = BillingInfoForm(instance=default_billing_info)
return kwargs
@login_required
@require_GET
def e_transaction_data(request):
basket = Basket.from_session(request.session)
if basket is None:
return HttpResponse(status=404, content=json.dumps({"data": []}))
data = basket.get_e_transaction_data()
data = {"data": [{"key": key, "value": val} for key, val in data]}
return HttpResponse(status=200, content=json.dumps(data))
@login_required
@require_POST
def pay_with_sith(request):
basket = Basket.from_session(request.session)
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
if basket is None or basket.items.filter(type_id=refilling).exists():
return redirect("eboutic:main")
c = Customer.objects.filter(user__id=basket.user.id).first()
if c is None:
return redirect("eboutic:main")
if c.amount < basket.get_total():
res = redirect("eboutic:payment_result", "failure")
else:
eboutic = Counter.objects.filter(type="EBOUTIC").first()
sales = basket.generate_sales(eboutic, c.user, "SITH_ACCOUNT")
try:
with transaction.atomic():
for sale in sales:
sale.save()
basket.delete()
request.session.pop("basket_id", None)
res = redirect("eboutic:payment_result", "success")
except DatabaseError as e:
with sentry_sdk.push_scope() as scope:
scope.user = {"username": request.user.username}
scope.set_extra("someVariable", e.__repr__())
sentry_sdk.capture_message(
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
)
res = redirect("eboutic:payment_result", "failure")
res.delete_cookie("basket_items", "/eboutic")
return res
class EtransactionAutoAnswer(View):
# Response documentation
# https://www1.paybox.com/espace-integrateur-documentation/la-solution-paybox-system/gestion-de-la-reponse/
def get(self, request, *args, **kwargs):
required = {"Amount", "BasketID", "Error", "Sig"}
if not required.issubset(set(request.GET.keys())):
return HttpResponse("Bad arguments", status=400)
key = crypto.load_publickey(crypto.FILETYPE_PEM, settings.SITH_EBOUTIC_PUB_KEY)
cert = crypto.X509()
cert.set_pubkey(key)
sig = base64.b64decode(request.GET["Sig"])
try:
crypto.verify(
cert,
sig,
"&".join(request.META["QUERY_STRING"].split("&")[:-1]).encode("utf-8"),
"sha1",
)
except:
return HttpResponse("Bad signature", status=400)
# Payment authorized:
# * 'Error' is '00000'
# * 'Auto' is in the request
if request.GET["Error"] == "00000" and "Auto" in request.GET.keys():
try:
with transaction.atomic():
b = (
Basket.objects.select_for_update()
.filter(id=request.GET["BasketID"])
.first()
)
if b is None:
raise SuspiciousOperation("Basket does not exists")
if int(b.get_total() * 100) != int(request.GET["Amount"]):
raise SuspiciousOperation(
"Basket total and amount do not match"
)
i = Invoice()
i.user = b.user
i.payment_method = "CARD"
i.save()
for it in b.items.all():
InvoiceItem(
invoice=i,
product_id=it.product_id,
product_name=it.product_name,
type_id=it.type_id,
product_unit_price=it.product_unit_price,
quantity=it.quantity,
).save()
i.validate()
b.delete()
except Exception as e:
return HttpResponse(
"Basket processing failed with error: " + repr(e), status=500
)
return HttpResponse("Payment successful", status=200)
else:
return HttpResponse(
"Payment failed with error: " + request.GET["Error"], status=202
)