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 @@

{% trans %}Eboutic{% endtrans %}

+ {% 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")