diff --git a/counter/models.py b/counter/models.py
index 8581b19d..3515e081 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -47,6 +47,10 @@ from counter.fields import CurrencyField
from subscription.models import Subscription
+def get_eboutic() -> Counter:
+ return Counter.objects.filter(type="EBOUTIC").order_by("id").first()
+
+
class CustomerQuerySet(models.QuerySet):
def update_amount(self) -> int:
"""Update the amount of all customers selected by this queryset.
diff --git a/eboutic/models.py b/eboutic/models.py
index 1d12e182..6bd500e6 100644
--- a/eboutic/models.py
+++ b/eboutic/models.py
@@ -28,12 +28,20 @@ from django.utils.translation import gettext_lazy as _
from core.models import User
from counter.fields import CurrencyField
-from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling
+from counter.models import (
+ BillingInfo,
+ Counter,
+ Customer,
+ Product,
+ Refilling,
+ Selling,
+ get_eboutic,
+)
def get_eboutic_products(user: User) -> list[Product]:
products = (
- Counter.objects.get(type="EBOUTIC")
+ get_eboutic()
.products.filter(product_type__isnull=False)
.filter(archived=False)
.filter(limit_age__lte=user.age)
@@ -102,13 +110,6 @@ class Basket(models.Model):
)["total"]
)
- @classmethod
- def from_session(cls, session) -> Basket | None:
- """The basket stored in the session object, if it exists."""
- if "basket_id" in session:
- return cls.objects.filter(id=session["basket_id"]).first()
- return None
-
def generate_sales(self, counter, seller: User, payment_method: str):
"""Generate a list of sold items corresponding to the items
of this basket WITHOUT saving them NOR deleting the basket.
diff --git a/eboutic/templates/eboutic/eboutic_payment_result.jinja b/eboutic/templates/eboutic/eboutic_payment_result.jinja
index 6c03754d..719ebc58 100644
--- a/eboutic/templates/eboutic/eboutic_payment_result.jinja
+++ b/eboutic/templates/eboutic/eboutic_payment_result.jinja
@@ -4,6 +4,14 @@
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+
{% if success %}
{% trans %}Payment successful{% endtrans %}
{% else %}
diff --git a/eboutic/tests/test_basket.py b/eboutic/tests/test_basket.py
index 856a0784..3e204d16 100644
--- a/eboutic/tests/test_basket.py
+++ b/eboutic/tests/test_basket.py
@@ -1,3 +1,4 @@
+import pytest
from django.http import HttpResponse
from django.test import TestCase
from django.test.client import Client
@@ -9,11 +10,20 @@ from pytest_django.asserts import assertRedirects
from core.baker_recipes import subscriber_user
from core.models import Group, User
from counter.baker_recipes import product_recipe
-from counter.models import Counter, ProductType
+from counter.models import Counter, ProductType, get_eboutic
from counter.tests.test_counter import BasketItem
from eboutic.models import Basket
+@pytest.mark.django_db
+def test_get_eboutic():
+ assert Counter.objects.get(name="Eboutic") == get_eboutic()
+
+ baker.make(Counter, type="EBOUTIC")
+
+ assert Counter.objects.get(name="Eboutic") == get_eboutic()
+
+
class TestEboutic(TestCase):
@classmethod
def setUpTestData(cls):
@@ -51,7 +61,7 @@ class TestEboutic(TestCase):
cls.new_customer.groups.add(cls.group_public)
cls.new_customer_adult.groups.add(cls.group_public)
- cls.eboutic = Counter.objects.get(name="Eboutic")
+ cls.eboutic = get_eboutic()
cls.eboutic.products.add(cls.cotiz, cls.beer, cls.snack)
@classmethod
diff --git a/eboutic/tests/test_payment.py b/eboutic/tests/test_payment.py
new file mode 100644
index 00000000..a3ce15c1
--- /dev/null
+++ b/eboutic/tests/test_payment.py
@@ -0,0 +1,122 @@
+from decimal import Decimal
+
+from django.contrib.messages import get_messages
+from django.contrib.messages.constants import DEFAULT_LEVELS
+from django.test import TestCase
+from django.urls import reverse
+from model_bakery import baker
+from pytest_django.asserts import assertRedirects
+
+from core.baker_recipes import subscriber_user
+from counter.baker_recipes import product_recipe
+from counter.models import Product, ProductType
+from counter.tests.test_counter import force_refill_user
+from eboutic.models import Basket, BasketItem
+
+
+class TestPaymentBase(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.customer = subscriber_user.make()
+ cls.basket = baker.make(Basket, user=cls.customer)
+ cls.refilling = Product.objects.get(code="15REFILL")
+
+ product_type = baker.make(ProductType)
+
+ cls.snack = product_recipe.make(
+ selling_price=1.5, special_selling_price=1, product_type=product_type
+ )
+ cls.beer = product_recipe.make(
+ limit_age=18,
+ selling_price=2.5,
+ special_selling_price=1,
+ product_type=product_type,
+ )
+
+ BasketItem.from_product(cls.snack, 1, cls.basket).save()
+ BasketItem.from_product(cls.beer, 2, cls.basket).save()
+
+
+class TestPaymentSith(TestPaymentBase):
+ def test_anonymous(self):
+ assert (
+ self.client.post(
+ reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id}),
+ ).status_code
+ == 403
+ )
+ assert Basket.objects.filter(id=self.basket.id).first() is not None
+
+ def test_unauthorized(self):
+ self.client.force_login(subscriber_user.make())
+ assert (
+ self.client.post(
+ reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id}),
+ ).status_code
+ == 403
+ )
+ assert Basket.objects.filter(id=self.basket.id).first() is not None
+
+ def test_not_found(self):
+ self.client.force_login(self.customer)
+ assert (
+ self.client.post(
+ reverse(
+ "eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id + 1}
+ ),
+ ).status_code
+ == 404
+ )
+ assert Basket.objects.filter(id=self.basket.id).first() is not None
+
+ def test_buy_success(self):
+ self.client.force_login(self.customer)
+ force_refill_user(self.customer, self.basket.total + 1)
+ assertRedirects(
+ self.client.post(
+ reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id}),
+ ),
+ reverse("eboutic:payment_result", kwargs={"result": "success"}),
+ )
+ assert Basket.objects.filter(id=self.basket.id).first() is None
+ self.customer.customer.refresh_from_db()
+ assert self.customer.customer.amount == Decimal("1")
+
+ def test_not_enough_money(self):
+ self.client.force_login(self.customer)
+ response = self.client.post(
+ reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id}),
+ )
+ assertRedirects(
+ response,
+ reverse("eboutic:payment_result", kwargs={"result": "failure"}),
+ )
+
+ messages = list(get_messages(response.wsgi_request))
+ assert len(messages) == 1
+ assert messages[0].level == DEFAULT_LEVELS["ERROR"]
+ assert messages[0].message == "Solde insuffisant"
+
+ assert Basket.objects.filter(id=self.basket.id).first() is not None
+
+ def test_refilling_in_basket(self):
+ BasketItem.from_product(self.refilling, 1, self.basket).save()
+ self.client.force_login(self.customer)
+ force_refill_user(self.customer, self.basket.total)
+ response = self.client.post(
+ reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id}),
+ )
+ assertRedirects(
+ response,
+ reverse("eboutic:payment_result", kwargs={"result": "failure"}),
+ )
+
+ assert Basket.objects.filter(id=self.basket.id).first() is not None
+ messages = list(get_messages(response.wsgi_request))
+ assert messages[0].level == DEFAULT_LEVELS["ERROR"]
+ assert (
+ messages[0].message
+ == "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith"
+ )
+ self.customer.customer.refresh_from_db()
+ assert self.customer.customer.amount == self.basket.total
diff --git a/eboutic/tests/tests.py b/eboutic/tests/tests.py
index c61dd374..52ddca92 100644
--- a/eboutic/tests/tests.py
+++ b/eboutic/tests/tests.py
@@ -85,33 +85,6 @@ class TestEboutic(TestCase):
)
return url
- def test_buy_with_sith_account(self):
- self.client.force_login(self.subscriber)
- self.subscriber.customer.amount = 100 # give money before test
- self.subscriber.customer.save()
- basket = self.get_busy_basket(self.subscriber)
- amount = basket.total
- response = self.client.post(reverse("eboutic:pay_with_sith"))
- self.assertRedirects(response, "/eboutic/pay/success/")
- new_balance = Customer.objects.get(user=self.subscriber).amount
- assert float(new_balance) == 100 - amount
- expected = 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic'
- assert expected == self.client.cookies["basket_items"].OutputString()
-
- def test_buy_with_sith_account_no_money(self):
- self.client.force_login(self.subscriber)
- basket = self.get_busy_basket(self.subscriber)
- initial = basket.total - 1 # just not enough to complete the sale
- self.subscriber.customer.amount = initial
- self.subscriber.customer.save()
- response = self.client.post(reverse("eboutic:pay_with_sith"))
- self.assertRedirects(response, "/eboutic/pay/failure/")
- new_balance = Customer.objects.get(user=self.subscriber).amount
- assert float(new_balance) == initial
- # this cookie should be removed after payment
- expected = 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic'
- assert expected == self.client.cookies["basket_items"].OutputString()
-
def test_buy_subscribe_product_with_credit_card(self):
self.client.force_login(self.old_subscriber)
response = self.client.get(
diff --git a/eboutic/views.py b/eboutic/views.py
index 8abd9ea2..382cd507 100644
--- a/eboutic/views.py
+++ b/eboutic/views.py
@@ -32,7 +32,7 @@ from django.contrib.auth.mixins import (
LoginRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
-from django.core.exceptions import SuspiciousOperation
+from django.core.exceptions import SuspiciousOperation, ValidationError
from django.db import DatabaseError, transaction
from django.db.models.fields import forms
from django.db.utils import cached_property
@@ -48,7 +48,7 @@ from django_countries.fields import Country
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BaseBasketForm, BillingInfoForm, ProductForm
-from counter.models import BillingInfo, Counter, Customer, Product, Selling
+from counter.models import BillingInfo, Customer, Product, Selling, get_eboutic
from eboutic.models import (
Basket,
BasketItem,
@@ -90,7 +90,7 @@ class EbouticMainView(LoginRequiredMixin, FormView):
kwargs = super().get_form_kwargs()
kwargs["form_kwargs"] = {
"customer": self.customer,
- "counter": Counter.objects.get(type="EBOUTIC"),
+ "counter": get_eboutic(),
"allowed_products": {product.id: product for product in self.products},
}
return kwargs
@@ -246,9 +246,9 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
self.request,
_("You can't buy a refilling with sith money"),
)
- return redirect("eboutic:main")
+ return redirect("eboutic:payment_result", "failure")
- eboutic = Counter.objects.get(type="EBOUTIC")
+ eboutic = get_eboutic()
sales = basket.generate_sales(eboutic, basket.user, "SITH_ACCOUNT")
try:
with transaction.atomic():
@@ -260,6 +260,8 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
return redirect("eboutic:payment_result", "success")
except DatabaseError as e:
sentry_sdk.capture_exception(e)
+ except ValidationError as e:
+ messages.error(self.request, e.message)
return redirect("eboutic:payment_result", "failure")