Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] ae0841808b [UPDATE] Update phonenumbers requirement
Updates the requirements on [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) to permit the latest version.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.30...v9.0.31)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.31
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 12:19:43 +00:00
12 changed files with 63 additions and 229 deletions
+1
View File
@@ -141,6 +141,7 @@ form {
display: block;
margin: calc(var(--nf-input-size) * 1.5) auto 10px;
line-height: 1;
white-space: nowrap;
.fields-centered {
padding: 10px 10px 0;
+1 -10
View File
@@ -1,6 +1,3 @@
from typing import Any
from ninja import Status
from ninja_extra import ControllerBase, api_controller, route
from ninja_extra.exceptions import NotFound
@@ -11,19 +8,13 @@ from eboutic.models import Basket
@api_controller("/etransaction", permissions=[CanView])
class EtransactionInfoController(ControllerBase):
@route.get(
"/data/{basket_id}",
url_name="etransaction_data",
response={200: dict[str, Any], 410: str},
)
@route.get("/data/{basket_id}", url_name="etransaction_data")
def fetch_etransaction_data(self, basket_id: int):
"""Generate the data to pay an eboutic command with paybox.
The data is generated with the basket that is used by the current session.
"""
basket: Basket = self.get_object_or_exception(Basket, pk=basket_id)
if basket.is_expired:
return Status(410, "This basket is expired.")
try:
return dict(basket.get_e_transaction_data())
except BillingInfo.DoesNotExist as e:
+7 -26
View File
@@ -24,7 +24,6 @@ from django.conf import settings
from django.db import DataError, models
from django.db.models import F, OuterRef, Subquery, Sum
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from core.models import User
@@ -96,10 +95,6 @@ class Basket(models.Model):
]
)
@property
def is_expired(self) -> bool:
return (self.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT) <= now()
def generate_sales(
self, counter, seller: User, payment_method: Selling.PaymentMethod
):
@@ -138,20 +133,9 @@ class Basket(models.Model):
]
def get_e_transaction_data(self) -> list[tuple[str, str]]:
"""Get data for etransaction payment.
Raises:
Customer.DoesNotExist: if the user linked to this basket
has no customer account
BillingInfo.DoesNotExist: if the user linked to this basket has no
billing infos, or incorrect billing infos.
ValueError: if this is called on a basket which payment delay is expired.
"""
user = self.user
if not hasattr(user, "customer"):
raise Customer.DoesNotExist
if self.is_expired:
raise ValueError("This method cannot be called on an expired basket.")
customer = user.customer
if (
not hasattr(user.customer, "billing_infos")
@@ -171,10 +155,6 @@ class Basket(models.Model):
("PBX_IDENTIFIANT", settings.SITH_EBOUTIC_PBX_IDENTIFIANT),
("PBX_TOTAL", str(int(self.total * 100))),
("PBX_DEVISE", "978"), # This is Euro
(
"PBX_DISPLAY",
str(int(settings.SITH_EBOUTIC_ETRANSACTION_TIMEOUT.total_seconds())),
),
("PBX_CMD", str(self.id)),
("PBX_PORTEUR", user.email),
("PBX_RETOUR", "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"),
@@ -239,14 +219,16 @@ class Invoice(models.Model):
if self.validated:
raise DataError(_("Invoice already validated"))
customer, _created = Customer.get_or_create(user=self.user)
kwargs = {"counter": get_eboutic(), "customer": customer, "date": self.date}
kwargs = {
"counter": get_eboutic(),
"customer": customer,
"date": self.date,
"payment_method": Selling.PaymentMethod.CARD,
}
for i in self.items.select_related("product"):
if i.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
Refilling.objects.create(
**kwargs,
operator=self.user,
amount=i.unit_price * i.quantity,
payment_method=Refilling.PaymentMethod.CARD,
**kwargs, operator=self.user, amount=i.unit_price * i.quantity
)
else:
Selling.objects.create(
@@ -257,7 +239,6 @@ class Invoice(models.Model):
seller=self.user,
unit_price=i.unit_price,
quantity=i.quantity,
payment_method=Selling.PaymentMethod.CARD,
)
self.validated = True
self.save()
@@ -1,71 +1,21 @@
import { type Notification, NotificationLevel } from "#core:utils/notifications";
import { etransactioninfoFetchEtransactionData } from "#openapi";
interface Basket {
id: number;
timeout: Date;
}
document.addEventListener("alpine:init", () => {
Alpine.data("etransaction", (initialData, basket: Basket) => ({
Alpine.data("etransaction", (initialData, basketId: number) => ({
data: initialData,
isCbAvailable: Object.keys(initialData).length > 0,
isSithAvailable: true,
init() {
const now = new Date();
const timeout = basket.timeout.getTime() - now.getTime();
if (timeout <= 0) {
// basket was already outdated at initial page load
this.timeoutBasket();
} else {
setTimeout(() => this.timeoutBasket(), timeout);
}
},
/**
* Make this basket into a timeout state.
* All submission inputs are disabled, and an error message is displayed.
*/
timeoutBasket() {
this.isCbAvailable = false;
this.isSithAvailable = false;
const message = gettext("Basket expired");
const existingNotif: Notification | undefined = this.$notifications
.getAll()
.find(
(n: Notification) =>
n.tag === NotificationLevel.Error && n.message === message,
);
if (existingNotif === undefined) {
this.$notifications.error(message);
}
},
/**
* Refresh the data used for etransaction.
*
* Note: if this is called while the basket is expired, it will be a no-op
*/
async fill() {
if (new Date() > basket.timeout) {
// refresh etransaction data only if the basket is still valid.
this.timeoutBasket();
return;
}
this.isCbAvailable = false;
const res = await etransactioninfoFetchEtransactionData({
path: {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
path: { basket_id: basket.id },
basket_id: basketId,
},
});
if (res.response.ok) {
this.data = res.data;
this.isCbAvailable = true;
} else if (res.response.status === 410) {
// The basket is expired, so no payment method should be available at all.
// This shouldn't happen, because we don't send the request
// when the timeout is passed, but we are better safe than sorry
this.timeoutBasket();
}
},
}));
@@ -21,7 +21,6 @@
hx-swap="outerHTML"
hx-target="#billing-infos-fragment"
x-show="collapsed"
x-cloak
>
{% csrf_token %}
{{ form.as_p() }}
@@ -15,17 +15,18 @@
{% block content %}
<h3>{% trans %}Eboutic{% endtrans %}</h3>
<div x-data='etransaction(
{{ billing_infos|tojson }},
{ id: {{ basket.id }}, timeout: new Date('{{ basket.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT }}') }
)'>
<script type="text/javascript">
let billingInfos = {{ billing_infos|safe }};
</script>
<div x-data="etransaction(billingInfos, {{ basket.id }})">
<p>{% trans %}Basket: {% endtrans %}</p>
<table>
<thead>
<tr>
<td>Article</td>
<td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Unit price{% endtrans %}</td>
<td>Quantity</td>
<td>Unit price</td>
</tr>
</thead>
<tbody>
@@ -59,21 +60,11 @@
<div @htmx:after-request="fill">
{{ billing_infos_form }}
</div>
{% endif %}
{% include "core/base/notifications.jinja" %}
<form method="post" id="payment-form">
{# In order to have one CGV button for both payment means,
there is only one form, which action will be given
the `formaction` attribute of the selected submit input #}
<div class="form-group margin-bottom">
<input type="checkbox" id="cgv" name="cgv" required>
<label for="cgv">
{% trans trimmed %}I have read and I accept{% endtrans %}
<a href="{{ url('core:page', 'cgv') }}">{% trans %}the general terms and conditions{% endtrans%}</a>
{%trans%}of the student association of the UTBM{% endtrans %}
</label>
</div>
{% if settings.SITH_EBOUTIC_CB_ENABLED %}
<form
method="post"
action="{{ settings.SITH_EBOUTIC_ET_URL }}"
>
<template x-for="[key, value] in Object.entries(data)" :key="key">
<input type="hidden" :name="key" :value="value">
</template>
@@ -81,15 +72,11 @@
x-cloak
type="submit"
id="bank-submit-button"
{% if basket.is_expired %}
disabled="disabled"
{% else %}
:disabled="!isCbAvailable"
{% endif %}
class="btn btn-blue"
formaction="{{ settings.SITH_EBOUTIC_ET_URL }}"
value="{% trans %}Pay with credit card{% endtrans %}"
/>
</form>
{% else %}
<div class="alert alert-yellow">
{% trans trimmed %}
@@ -104,19 +91,10 @@
{% elif basket.total > user.account_balance %}
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
{% else %}
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
{% csrf_token %}
<input
{% if basket.is_expired %}
disabled="disabled"
{% else %}
:disabled="!isSithAvailable"
{% endif %}
class="btn btn-blue"
type="submit"
formaction="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}"
value="{% trans %}Pay with Sith account{% endtrans %}"
/>
{% endif %}
<input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
</form>
{% endif %}
</div>
{% endblock %}
+7 -27
View File
@@ -3,7 +3,6 @@ import urllib
from decimal import Decimal
from typing import TYPE_CHECKING
import freezegun
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.hashes import SHA1
@@ -18,7 +17,7 @@ from pytest_django.asserts import assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user
from counter.baker_recipes import price_recipe, product_recipe
from counter.models import Product, ProductType, Refilling, Selling
from counter.models import Product, ProductType, Selling
from counter.tests.test_counter import force_refill_user
from eboutic.models import Basket, BasketItem
@@ -106,7 +105,7 @@ class TestPaymentSith(TestPaymentBase):
),
reverse("eboutic:payment_result", kwargs={"result": "success"}),
)
assert not Basket.objects.filter(id=self.basket.id).exists()
assert Basket.objects.filter(id=self.basket.id).first() is None
self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == Decimal(1)
@@ -140,7 +139,10 @@ class TestPaymentSith(TestPaymentBase):
assert len(messages) == 1
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
assert messages[0].message == "Solde insuffisant"
assert not Basket.objects.filter(id=self.basket.id).exists()
assert Basket.objects.contains(self.basket), (
"After an unsuccessful request, the basket should be kept"
)
def test_refilling_in_basket(self):
BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save()
@@ -155,7 +157,7 @@ class TestPaymentSith(TestPaymentBase):
response,
reverse("eboutic:payment_result", kwargs={"result": "failure"}),
)
assert not Basket.objects.filter(id=self.basket.id).exists()
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 (
@@ -165,24 +167,6 @@ class TestPaymentSith(TestPaymentBase):
self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == initial_account_balance
def test_basket_expired(self):
self.client.force_login(self.customer)
initial_account_balance = self.customer.customer.amount
with freezegun.freeze_time(settings.SITH_EBOUTIC_BASKET_TIMEOUT):
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 messages[0].level == DEFAULT_LEVELS["ERROR"]
assert messages[0].message == "Panier expiré"
assert not Basket.objects.filter(id=self.basket.id).exists()
self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == initial_account_balance
class TestPaymentCard(TestPaymentBase):
def generate_bank_valid_answer(self, basket: Basket):
@@ -252,10 +236,6 @@ class TestPaymentCard(TestPaymentBase):
self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == price.amount * 2
refill = self.customer.customer.refillings.last()
assert refill is not None
assert refill.amount == price.amount * 2
assert refill.payment_method == Refilling.PaymentMethod.CARD
def test_multiple_responses(self):
bank_response = self.generate_bank_valid_answer(self.basket)
+3 -18
View File
@@ -39,8 +39,6 @@ from django.db.utils import cached_property
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.formats import localize
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_GET
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
@@ -189,7 +187,9 @@ class BillingInfoFormFragment(
def get_initial(self):
if self.object is None:
return {"country": Country(code="FR")}
return {
"country": Country(code="FR"),
}
return {}
def render_fragment(self, request, **kwargs) -> SafeString:
@@ -255,15 +255,6 @@ class EbouticCheckout(CanViewMixin, UseFragmentsMixin, DetailView):
kwargs["customer_amount"] = None
kwargs["billing_infos"] = {}
if self.object.is_expired:
messages.error(self.request, _("Basket expired"))
else:
timeout = self.object.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT
messages.warning(
self.request,
_("Basket available until %(until)s")
% {"until": localize(localtime(timeout).time())},
)
with contextlib.suppress(BillingInfo.DoesNotExist):
kwargs["billing_infos"] = json.dumps(
dict(self.object.get_e_transaction_data())
@@ -277,14 +268,9 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
basket = self.get_object()
if basket.is_expired:
messages.error(self.request, _("Basket expired"))
basket.delete()
return redirect("eboutic:payment_result", "failure")
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
if basket.items.filter(product__product_type_id=refilling).exists():
messages.error(self.request, _("You can't buy a refilling with sith money"))
basket.delete()
return redirect("eboutic:payment_result", "failure")
eboutic = get_eboutic()
@@ -302,7 +288,6 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
except DatabaseError as e:
sentry_sdk.capture_exception(e)
except ValidationError as e:
basket.delete()
messages.error(self.request, e.message)
return redirect("eboutic:payment_result", "failure")
+2 -24
View File
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-30 20:12+0200\n"
"POT-Creation-Date: 2026-05-12 11:12+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -683,7 +683,6 @@ msgstr "Étiquette"
#: core/templates/core/user_account_detail.jinja
#: core/templates/core/user_stats.jinja
#: counter/templates/counter/last_ops.jinja
#: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "Quantity"
msgstr "Quantité"
@@ -967,7 +966,7 @@ msgstr "rôle de club membre"
msgid "Benefit"
msgstr "Bénéfice"
#: club/views.py eboutic/templates/eboutic/eboutic_checkout.jinja
#: club/views.py
msgid "Unit price"
msgstr "Prix unitaire"
@@ -4368,18 +4367,6 @@ msgstr "Solde actuel : "
msgid "Remaining account amount: "
msgstr "Solde restant : "
#: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "I have read and I accept"
msgstr "J'ai lu et j'accepte"
#: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "the general terms and conditions"
msgstr "les conditions générales de vente"
#: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "of the student association of the UTBM"
msgstr "de l'Association des étudiants de l'UTBM"
#: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "Pay with credit card"
msgstr "Payer avec une carte bancaire"
@@ -4518,15 +4505,6 @@ msgstr ""
"souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux "
"données que vous aviez déjà fourni."
#: eboutic/views.py
msgid "Basket expired"
msgstr "Panier expiré"
#: eboutic/views.py
#, python-format
msgid "Basket available until %(until)s"
msgstr "Panier disponible jusqu'à %(until)s"
#: eboutic/views.py
msgid "You can't buy a refilling with sith money"
msgstr "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith"
+1 -5
View File
@@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-17 10:03+0200\n"
"POT-Creation-Date: 2026-04-17 22:42+0200\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -263,10 +263,6 @@ msgstr "Types de produits réordonnés !"
msgid "Product type reorganisation failed with status code : %d"
msgstr "La réorganisation des types de produit a échoué avec le code : %d"
#: eboutic/static/bundled/eboutic/checkout-index.ts
msgid "Basket expired"
msgstr "Panier expiré"
#: sas/static/bundled/sas/pictures-download-index.ts
msgid "pictures.%(extension)s"
msgstr "photos.%(extension)s"
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
"django-jinja<3.0.0,>=2.11.0",
"cryptography>=48.0.0,<49.0.0",
"django-phonenumber-field>=8.4.0,<9.0.0",
"phonenumbers>=9.0.30,<10.0.0",
"phonenumbers>=9.0.31,<10.0.0",
"reportlab>=4.5.1,<5.0.0",
"django-haystack>=3.3.0,<4.0.0",
"xapian-haystack>=4.0.0,<5.0.0",
-5
View File
@@ -571,11 +571,6 @@ SITH_BARMAN_TIMEOUT = 30
# Minutes to delete the last operations
SITH_LAST_OPERATIONS_LIMIT = 10
# time before a basket is considered expired
SITH_EBOUTIC_BASKET_TIMEOUT = timedelta(minutes=10)
# time that a user can spend on the CB payment page before it to timeout
SITH_EBOUTIC_ETRANSACTION_TIMEOUT = timedelta(minutes=10)
# ET variables
SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True)
SITH_EBOUTIC_ET_URL = env.str(