mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-22 08:50:17 +00:00
Compare commits
2 Commits
taiste
...
basket-timeout
| Author | SHA1 | Date | |
|---|---|---|---|
| 26585aa521 | |||
| 99600341b0 |
@@ -4,7 +4,7 @@
|
|||||||
<title>{% trans %}Slideshow{% endtrans %}</title>
|
<title>{% trans %}Slideshow{% endtrans %}</title>
|
||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
|
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
|
||||||
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="slideshow([
|
<body x-data="slideshow([
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import sort from "@alpinejs/sort";
|
|
||||||
import Alpine from "alpinejs";
|
|
||||||
import { limitedChoices } from "#core:alpine/limited-choices.ts";
|
|
||||||
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications.ts";
|
|
||||||
|
|
||||||
Alpine.plugin([sort, limitedChoices]);
|
|
||||||
Alpine.magic("notifications", notificationPlugin);
|
|
||||||
window.Alpine = Alpine;
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
Alpine.start();
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import sort from "@alpinejs/sort";
|
||||||
|
import { Alpine } from "alpinejs";
|
||||||
|
import { limitedChoices } from "#core:alpine/limited-choices";
|
||||||
|
import {
|
||||||
|
type NotificationPlugin,
|
||||||
|
alpinePlugin as notificationPlugin,
|
||||||
|
} from "#core:utils/notifications";
|
||||||
|
|
||||||
|
declare module "alpinejs" {
|
||||||
|
interface Magics<T> {
|
||||||
|
$notifications: NotificationPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Alpine.plugin([sort, limitedChoices]);
|
||||||
|
Alpine.magic("notifications", notificationPlugin);
|
||||||
|
// biome-ignore lint/style/useNamingConvention: it's how it's named
|
||||||
|
Object.assign(window, { Alpine });
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
Alpine.start();
|
||||||
|
});
|
||||||
@@ -1,30 +1,40 @@
|
|||||||
|
import { Alpine } from "alpinejs";
|
||||||
|
|
||||||
export enum NotificationLevel {
|
export enum NotificationLevel {
|
||||||
Error = "error",
|
Error = "error",
|
||||||
Warning = "warning",
|
Warning = "warning",
|
||||||
Success = "success",
|
Success = "success",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNotification(message: string, level: NotificationLevel) {
|
export interface NotificationPlugin {
|
||||||
const element = document.getElementById("quick-notifications");
|
error: (message: string) => void;
|
||||||
if (element === null) {
|
warning: (message: string) => void;
|
||||||
return false;
|
success: (message: string) => void;
|
||||||
}
|
clear: () => void;
|
||||||
return element.dispatchEvent(
|
addMany: (notifs: Notification[]) => void;
|
||||||
new CustomEvent("quick-notification-add", {
|
getAll: () => Notification[];
|
||||||
detail: { text: message, tag: level },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteNotifications() {
|
export interface Notification {
|
||||||
const element = document.getElementById("quick-notifications");
|
tag: NotificationLevel;
|
||||||
if (element === null) {
|
text: string;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return element.dispatchEvent(new CustomEvent("quick-notification-delete"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function alpinePlugin() {
|
Alpine.store("notifications", [] as Notification[]);
|
||||||
|
|
||||||
|
function createNotification(message: string, level: NotificationLevel) {
|
||||||
|
(Alpine.store("notifications") as Notification[]).push({ text: message, tag: level });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteNotifications() {
|
||||||
|
Alpine.store("notifications", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotifications() {
|
||||||
|
return Alpine.store("notifications") as Notification[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function alpinePlugin(): NotificationPlugin {
|
||||||
return {
|
return {
|
||||||
error: (message: string) => createNotification(message, NotificationLevel.Error),
|
error: (message: string) => createNotification(message, NotificationLevel.Error),
|
||||||
warning: (message: string) =>
|
warning: (message: string) =>
|
||||||
@@ -32,5 +42,10 @@ export function alpinePlugin() {
|
|||||||
success: (message: string) =>
|
success: (message: string) =>
|
||||||
createNotification(message, NotificationLevel.Success),
|
createNotification(message, NotificationLevel.Success),
|
||||||
clear: () => deleteNotifications(),
|
clear: () => deleteNotifications(),
|
||||||
|
addMany: (notifs: Notification[]) =>
|
||||||
|
notifs.forEach((n) => {
|
||||||
|
createNotification(n.text, n.tag);
|
||||||
|
}),
|
||||||
|
getAll: () => getNotifications(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<script src="{{ url('javascript-catalog') }}"></script>
|
<script src="{{ url('javascript-catalog') }}"></script>
|
||||||
<script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script>
|
<script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script>
|
||||||
<script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script>
|
<script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<div id="quick-notifications"
|
<div id="quick-notifications"
|
||||||
x-data='{
|
x-data='{ messages: $notifications.getAll() }'
|
||||||
messages: [
|
x-init='$notifications.addMany([
|
||||||
{%- for message in messages -%}
|
{%- for message in messages -%}
|
||||||
{%- if not message.extra_tags -%}
|
{%- if not message.extra_tags -%}
|
||||||
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} },
|
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} },
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
]
|
])'
|
||||||
}'
|
>
|
||||||
@quick-notification-add="(e) => messages.push(e?.detail)"
|
|
||||||
@quick-notification-delete="messages = []">
|
|
||||||
<template x-for="(message, index) in messages">
|
<template x-for="(message, index) in messages">
|
||||||
<div class="alert" :class="`alert-${message.tag}`" x-transition>
|
<div class="alert" :class="`alert-${message.tag}`" x-transition>
|
||||||
<span class="alert-main" x-text="message.text"></span>
|
<span class="alert-main" x-text="message.text"></span>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ les fichiers sont à mettre dans un dossier `static/bundled` de l'application à
|
|||||||
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `bundled/` comme prefix.
|
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `bundled/` comme prefix.
|
||||||
|
|
||||||
```jinja
|
```jinja
|
||||||
{# Example pour ajouter sith/core/bundled/alpine-index.js #}
|
{# Example pour ajouter sith/core/bundled/alpine-index.ts #}
|
||||||
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/other-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/other-index.ts') }}"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -1,3 +1,6 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ninja import Status
|
||||||
from ninja_extra import ControllerBase, api_controller, route
|
from ninja_extra import ControllerBase, api_controller, route
|
||||||
from ninja_extra.exceptions import NotFound
|
from ninja_extra.exceptions import NotFound
|
||||||
|
|
||||||
@@ -8,13 +11,19 @@ from eboutic.models import Basket
|
|||||||
|
|
||||||
@api_controller("/etransaction", permissions=[CanView])
|
@api_controller("/etransaction", permissions=[CanView])
|
||||||
class EtransactionInfoController(ControllerBase):
|
class EtransactionInfoController(ControllerBase):
|
||||||
@route.get("/data/{basket_id}", url_name="etransaction_data")
|
@route.get(
|
||||||
|
"/data/{basket_id}",
|
||||||
|
url_name="etransaction_data",
|
||||||
|
response={200: dict[str, Any], 410: str},
|
||||||
|
)
|
||||||
def fetch_etransaction_data(self, basket_id: int):
|
def fetch_etransaction_data(self, basket_id: int):
|
||||||
"""Generate the data to pay an eboutic command with paybox.
|
"""Generate the data to pay an eboutic command with paybox.
|
||||||
|
|
||||||
The data is generated with the basket that is used by the current session.
|
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)
|
basket: Basket = self.get_object_or_exception(Basket, pk=basket_id)
|
||||||
|
if basket.is_expired:
|
||||||
|
return Status(410, "This basket is expired.")
|
||||||
try:
|
try:
|
||||||
return dict(basket.get_e_transaction_data())
|
return dict(basket.get_e_transaction_data())
|
||||||
except BillingInfo.DoesNotExist as e:
|
except BillingInfo.DoesNotExist as e:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from django.conf import settings
|
|||||||
from django.db import DataError, models
|
from django.db import DataError, models
|
||||||
from django.db.models import F, OuterRef, Subquery, Sum
|
from django.db.models import F, OuterRef, Subquery, Sum
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
@@ -95,6 +96,10 @@ class Basket(models.Model):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
return (self.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT) <= now()
|
||||||
|
|
||||||
def generate_sales(
|
def generate_sales(
|
||||||
self, counter, seller: User, payment_method: Selling.PaymentMethod
|
self, counter, seller: User, payment_method: Selling.PaymentMethod
|
||||||
):
|
):
|
||||||
@@ -133,9 +138,20 @@ class Basket(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_e_transaction_data(self) -> list[tuple[str, str]]:
|
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
|
user = self.user
|
||||||
if not hasattr(user, "customer"):
|
if not hasattr(user, "customer"):
|
||||||
raise Customer.DoesNotExist
|
raise Customer.DoesNotExist
|
||||||
|
if self.is_expired:
|
||||||
|
raise ValueError("This method cannot be called on an expired basket.")
|
||||||
customer = user.customer
|
customer = user.customer
|
||||||
if (
|
if (
|
||||||
not hasattr(user.customer, "billing_infos")
|
not hasattr(user.customer, "billing_infos")
|
||||||
@@ -155,6 +171,10 @@ class Basket(models.Model):
|
|||||||
("PBX_IDENTIFIANT", settings.SITH_EBOUTIC_PBX_IDENTIFIANT),
|
("PBX_IDENTIFIANT", settings.SITH_EBOUTIC_PBX_IDENTIFIANT),
|
||||||
("PBX_TOTAL", str(int(self.total * 100))),
|
("PBX_TOTAL", str(int(self.total * 100))),
|
||||||
("PBX_DEVISE", "978"), # This is Euro
|
("PBX_DEVISE", "978"), # This is Euro
|
||||||
|
(
|
||||||
|
"PBX_DISPLAY",
|
||||||
|
str(int(settings.SITH_EBOUTIC_ETRANSACTION_TIMEOUT.total_seconds())),
|
||||||
|
),
|
||||||
("PBX_CMD", str(self.id)),
|
("PBX_CMD", str(self.id)),
|
||||||
("PBX_PORTEUR", user.email),
|
("PBX_PORTEUR", user.email),
|
||||||
("PBX_RETOUR", "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"),
|
("PBX_RETOUR", "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"),
|
||||||
|
|||||||
@@ -1,21 +1,71 @@
|
|||||||
|
import { type Notification, NotificationLevel } from "#core:utils/notifications";
|
||||||
import { etransactioninfoFetchEtransactionData } from "#openapi";
|
import { etransactioninfoFetchEtransactionData } from "#openapi";
|
||||||
|
|
||||||
|
interface Basket {
|
||||||
|
id: number;
|
||||||
|
timeout: Date;
|
||||||
|
}
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("etransaction", (initialData, basketId: number) => ({
|
Alpine.data("etransaction", (initialData, basket: Basket) => ({
|
||||||
data: initialData,
|
data: initialData,
|
||||||
isCbAvailable: Object.keys(initialData).length > 0,
|
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() {
|
async fill() {
|
||||||
|
if (new Date() > basket.timeout) {
|
||||||
|
// refresh etransaction data only if the basket is still valid.
|
||||||
|
this.timeoutBasket();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isCbAvailable = false;
|
this.isCbAvailable = false;
|
||||||
const res = await etransactioninfoFetchEtransactionData({
|
const res = await etransactioninfoFetchEtransactionData({
|
||||||
path: {
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
path: { basket_id: basket.id },
|
||||||
basket_id: basketId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (res.response.ok) {
|
if (res.response.ok) {
|
||||||
this.data = res.data;
|
this.data = res.data;
|
||||||
this.isCbAvailable = true;
|
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,6 +21,7 @@
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#billing-infos-fragment"
|
hx-target="#billing-infos-fragment"
|
||||||
x-show="collapsed"
|
x-show="collapsed"
|
||||||
|
x-cloak
|
||||||
>
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p() }}
|
{{ form.as_p() }}
|
||||||
|
|||||||
@@ -15,11 +15,10 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Eboutic{% endtrans %}</h3>
|
<h3>{% trans %}Eboutic{% endtrans %}</h3>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<div x-data='etransaction(
|
||||||
let billingInfos = {{ billing_infos|safe }};
|
{{ billing_infos|tojson }},
|
||||||
</script>
|
{ id: {{ basket.id }}, timeout: new Date('{{ basket.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT }}') }
|
||||||
|
)'>
|
||||||
<div x-data="etransaction(billingInfos, {{ basket.id }})">
|
|
||||||
<p>{% trans %}Basket: {% endtrans %}</p>
|
<p>{% trans %}Basket: {% endtrans %}</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -72,7 +71,11 @@
|
|||||||
x-cloak
|
x-cloak
|
||||||
type="submit"
|
type="submit"
|
||||||
id="bank-submit-button"
|
id="bank-submit-button"
|
||||||
:disabled="!isCbAvailable"
|
{% if basket.is_expired %}
|
||||||
|
disabled="disabled"
|
||||||
|
{% else %}
|
||||||
|
:disabled="!isCbAvailable"
|
||||||
|
{% endif %}
|
||||||
class="btn btn-blue"
|
class="btn btn-blue"
|
||||||
value="{% trans %}Pay with credit card{% endtrans %}"
|
value="{% trans %}Pay with credit card{% endtrans %}"
|
||||||
/>
|
/>
|
||||||
@@ -93,7 +96,16 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
|
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
<input
|
||||||
|
{% if basket.is_expired %}
|
||||||
|
disabled="disabled"
|
||||||
|
{% else %}
|
||||||
|
:disabled="!isSithAvailable"
|
||||||
|
{% endif %}
|
||||||
|
class="btn btn-blue"
|
||||||
|
type="submit"
|
||||||
|
value="{% trans %}Pay with Sith account{% endtrans %}"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import urllib
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import freezegun
|
||||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||||
from cryptography.hazmat.primitives.hashes import SHA1
|
from cryptography.hazmat.primitives.hashes import SHA1
|
||||||
@@ -105,7 +106,7 @@ class TestPaymentSith(TestPaymentBase):
|
|||||||
),
|
),
|
||||||
reverse("eboutic:payment_result", kwargs={"result": "success"}),
|
reverse("eboutic:payment_result", kwargs={"result": "success"}),
|
||||||
)
|
)
|
||||||
assert Basket.objects.filter(id=self.basket.id).first() is None
|
assert not Basket.objects.filter(id=self.basket.id).exists()
|
||||||
self.customer.customer.refresh_from_db()
|
self.customer.customer.refresh_from_db()
|
||||||
assert self.customer.customer.amount == Decimal(1)
|
assert self.customer.customer.amount == Decimal(1)
|
||||||
|
|
||||||
@@ -139,10 +140,7 @@ class TestPaymentSith(TestPaymentBase):
|
|||||||
assert len(messages) == 1
|
assert len(messages) == 1
|
||||||
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
|
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
|
||||||
assert messages[0].message == "Solde insuffisant"
|
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):
|
def test_refilling_in_basket(self):
|
||||||
BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save()
|
BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save()
|
||||||
@@ -157,7 +155,7 @@ class TestPaymentSith(TestPaymentBase):
|
|||||||
response,
|
response,
|
||||||
reverse("eboutic:payment_result", kwargs={"result": "failure"}),
|
reverse("eboutic:payment_result", kwargs={"result": "failure"}),
|
||||||
)
|
)
|
||||||
assert Basket.objects.filter(id=self.basket.id).first() is not None
|
assert not Basket.objects.filter(id=self.basket.id).exists()
|
||||||
messages = list(get_messages(response.wsgi_request))
|
messages = list(get_messages(response.wsgi_request))
|
||||||
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
|
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
|
||||||
assert (
|
assert (
|
||||||
@@ -167,6 +165,24 @@ class TestPaymentSith(TestPaymentBase):
|
|||||||
self.customer.customer.refresh_from_db()
|
self.customer.customer.refresh_from_db()
|
||||||
assert self.customer.customer.amount == initial_account_balance
|
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):
|
class TestPaymentCard(TestPaymentBase):
|
||||||
def generate_bank_valid_answer(self, basket: Basket):
|
def generate_bank_valid_answer(self, basket: Basket):
|
||||||
|
|||||||
+21
-6
@@ -39,6 +39,8 @@ from django.db.utils import cached_property
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
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.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_GET
|
from django.views.decorators.http import require_GET
|
||||||
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
|
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
|
||||||
@@ -187,9 +189,7 @@ class BillingInfoFormFragment(
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
if self.object is None:
|
if self.object is None:
|
||||||
return {
|
return {"country": Country(code="FR")}
|
||||||
"country": Country(code="FR"),
|
|
||||||
}
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def render_fragment(self, request, **kwargs) -> SafeString:
|
def render_fragment(self, request, **kwargs) -> SafeString:
|
||||||
@@ -255,10 +255,19 @@ class EbouticCheckout(CanViewMixin, UseFragmentsMixin, DetailView):
|
|||||||
kwargs["customer_amount"] = None
|
kwargs["customer_amount"] = None
|
||||||
kwargs["billing_infos"] = {}
|
kwargs["billing_infos"] = {}
|
||||||
|
|
||||||
with contextlib.suppress(BillingInfo.DoesNotExist):
|
if self.object.is_expired:
|
||||||
kwargs["billing_infos"] = json.dumps(
|
messages.error(self.request, _("Basket expired"))
|
||||||
dict(self.object.get_e_transaction_data())
|
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())
|
||||||
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@@ -268,9 +277,14 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
basket = self.get_object()
|
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
|
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||||
if basket.items.filter(product__product_type_id=refilling).exists():
|
if basket.items.filter(product__product_type_id=refilling).exists():
|
||||||
messages.error(self.request, _("You can't buy a refilling with sith money"))
|
messages.error(self.request, _("You can't buy a refilling with sith money"))
|
||||||
|
basket.delete()
|
||||||
return redirect("eboutic:payment_result", "failure")
|
return redirect("eboutic:payment_result", "failure")
|
||||||
|
|
||||||
eboutic = get_eboutic()
|
eboutic = get_eboutic()
|
||||||
@@ -288,6 +302,7 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
|||||||
except DatabaseError as e:
|
except DatabaseError as e:
|
||||||
sentry_sdk.capture_exception(e)
|
sentry_sdk.capture_exception(e)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
basket.delete()
|
||||||
messages.error(self.request, e.message)
|
messages.error(self.request, e.message)
|
||||||
return redirect("eboutic:payment_result", "failure")
|
return redirect("eboutic:payment_result", "failure")
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-05-12 09:52+0200\n"
|
"POT-Creation-Date: 2026-05-15 11:46+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -4441,6 +4441,15 @@ msgstr ""
|
|||||||
"souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux "
|
"souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux "
|
||||||
"données que vous aviez déjà fourni."
|
"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
|
#: eboutic/views.py
|
||||||
msgid "You can't buy a refilling with sith money"
|
msgid "You can't buy a refilling with sith money"
|
||||||
msgstr "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith"
|
msgstr "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-04-17 22:42+0200\n"
|
"POT-Creation-Date: 2026-05-17 10:03+0200\n"
|
||||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -263,6 +263,10 @@ msgstr "Types de produits réordonnés !"
|
|||||||
msgid "Product type reorganisation failed with status code : %d"
|
msgid "Product type reorganisation failed with status code : %d"
|
||||||
msgstr "La réorganisation des types de produit a échoué avec le 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
|
#: sas/static/bundled/sas/pictures-download-index.ts
|
||||||
msgid "pictures.%(extension)s"
|
msgid "pictures.%(extension)s"
|
||||||
msgstr "photos.%(extension)s"
|
msgstr "photos.%(extension)s"
|
||||||
|
|||||||
@@ -566,6 +566,11 @@ SITH_BARMAN_TIMEOUT = 30
|
|||||||
# Minutes to delete the last operations
|
# Minutes to delete the last operations
|
||||||
SITH_LAST_OPERATIONS_LIMIT = 10
|
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
|
# ET variables
|
||||||
SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True)
|
SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True)
|
||||||
SITH_EBOUTIC_ET_URL = env.str(
|
SITH_EBOUTIC_ET_URL = env.str(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class JsBundlerManifestEntry:
|
|||||||
# because that's what the user types when importing statics and that's what django gives us
|
# because that's what the user types when importing statics and that's what django gives us
|
||||||
# This is really similar to what we are doing in the bundler, it uses a similar algorithm
|
# This is really similar to what we are doing in the bundler, it uses a similar algorithm
|
||||||
# Example:
|
# Example:
|
||||||
# core/static/bundled/alpine-index.js -> bundled/alpine-index.js
|
# core/static/bundled/alpine-index.ts -> bundled/alpine-index.ts
|
||||||
# core/static/bundled/components/include-index.ts -> core/static/bundled/components/include-index.ts
|
# core/static/bundled/components/include-index.ts -> core/static/bundled/components/include-index.ts
|
||||||
def get_relative_src_name(name: str) -> str:
|
def get_relative_src_name(name: str) -> str:
|
||||||
original_path = Path(name)
|
original_path = Path(name)
|
||||||
|
|||||||
Reference in New Issue
Block a user