refactor eboutic command page

This commit is contained in:
Thomas Girod
2025-04-06 16:25:55 +02:00
parent e35c1d1928
commit d03c425a17
8 changed files with 90 additions and 92 deletions

View File

@ -26,7 +26,7 @@ class EtransactionInfoController(ControllerBase):
customer=customer, defaults=info.model_dump(exclude_none=True)
)
@route.get("/data", url_name="etransaction_data", include_in_schema=False)
@route.get("/data", url_name="etransaction_data")
def fetch_etransaction_data(self):
"""Generate the data to pay an eboutic command with paybox.

View File

@ -1,56 +1,61 @@
/**
* @readonly
* @enum {number}
*/
const BillingInfoReqState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SUCCESS: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
FAILURE: 2,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SENDING: 3,
};
import { exportToHtml } from "#core:utils/globals";
import {
type BillingInfoSchema,
etransactioninfoFetchEtransactionData,
etransactioninfoPutUserBillingInfo,
} from "#openapi";
enum BillingInfoReqState {
Success = "0",
Failure = "1",
Sending = "2",
}
exportToHtml("BillingInfoReqState", BillingInfoReqState);
document.addEventListener("alpine:init", () => {
Alpine.store("billing_inputs", {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
data: etData,
Alpine.data("etransactionData", (initialData) => ({
data: initialData,
async fill() {
const button = document.getElementById("bank-submit-button") as HTMLButtonElement;
button.disabled = true;
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(etDataUrl);
if (res.ok) {
this.data = await res.json();
const res = await etransactioninfoFetchEtransactionData();
if (res.response.ok) {
this.data = res.data;
button.disabled = false;
}
},
});
}));
Alpine.data("billing_infos", () => ({
Alpine.data("billing_infos", (userId: number) => ({
/** @type {BillingInfoReqState | null} */
reqState: null,
async sendForm() {
this.reqState = BillingInfoReqState.SENDING;
this.reqState = BillingInfoReqState.Sending;
const form = document.getElementById("billing_info_form");
document.getElementById("bank-submit-button").disabled = true;
const submitButton = document.getElementById(
"bank-submit-button",
) as HTMLButtonElement;
submitButton.disabled = true;
const payload = Object.fromEntries(
Array.from(form.querySelectorAll("input, select"))
.filter((elem) => elem.type !== "submit" && elem.value)
.map((elem) => [elem.name, elem.value]),
.filter((elem: HTMLInputElement) => elem.type !== "submit" && elem.value)
.map((elem: HTMLInputElement) => [elem.name, elem.value]),
);
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(billingInfoUrl, {
method: "PUT",
body: JSON.stringify(payload),
const res = await etransactioninfoPutUserBillingInfo({
// biome-ignore lint/style/useNamingConvention: API is snake_case
path: { user_id: userId },
body: payload as unknown as BillingInfoSchema,
});
this.reqState = res.ok
? BillingInfoReqState.SUCCESS
: BillingInfoReqState.FAILURE;
if (res.status === 422) {
const errors = (await res.json()).detail.flatMap((err) => err.loc);
this.reqState = res.response.ok
? BillingInfoReqState.Success
: BillingInfoReqState.Failure;
if (res.response.status === 422) {
const errors = await res.response
.json()
.detail.flatMap((err: Record<"loc", string>) => err.loc);
for (const elem of Array.from(form.querySelectorAll("input")).filter((elem) =>
errors.includes(elem.name),
)) {
@ -58,29 +63,27 @@ document.addEventListener("alpine:init", () => {
elem.reportValidity();
elem.oninput = () => elem.setCustomValidity("");
}
} else if (res.ok) {
Alpine.store("billing_inputs").fill();
} else if (res.response.ok) {
this.$dispatch("billing-infos-filled");
}
},
getAlertColor() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
if (this.reqState === BillingInfoReqState.Success) {
return "green";
}
if (this.reqState === BillingInfoReqState.FAILURE) {
if (this.reqState === BillingInfoReqState.Failure) {
return "red";
}
return "";
},
getAlertMessage() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoSuccessMessage;
if (this.reqState === BillingInfoReqState.Success) {
return gettext("Billing info registration success");
}
if (this.reqState === BillingInfoReqState.FAILURE) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoFailureMessage;
if (this.reqState === BillingInfoReqState.Failure) {
return gettext("Billing info registration failure");
}
return "";
},

View File

@ -158,4 +158,3 @@
flex-direction: column;
}
}

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block additional_js %}
<script src="{{ static('bundled/eboutic/makecommand-index.ts') }}" defer></script>
<script type="module" src="{{ static('bundled/eboutic/makecommand-index.ts') }}"></script>
{% endblock %}
{% block content %}
@ -56,7 +56,7 @@
<div
class="collapse"
:class="{'shadow': collapsed}"
x-data="{collapsed: !billingInfoExist}"
x-data="{collapsed: !{{ "true" if billing_infos else "false" }}}"
x-cloak
>
<div class="collapse-header clickable" @click="collapsed = !collapsed">
@ -70,7 +70,7 @@
<form
class="collapse-body"
id="billing_info_form"
x-data="billing_infos"
x-data="billing_infos({{ user.id }})"
x-show="collapsed"
x-transition.scale.origin.top
@submit.prevent="await sendForm()"
@ -79,7 +79,7 @@
{{ billing_form }}
<br />
<div
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(reqState)"
x-show="[BillingInfoReqState.Success, BillingInfoReqState.Failure].includes(reqState)"
class="alert"
:class="'alert-' + getAlertColor()"
x-transition
@ -92,19 +92,20 @@
<input
type="submit" class="btn btn-blue clickable"
value="{% trans %}Validate{% endtrans %}"
:disabled="reqState === BillingInfoReqState.SENDING"
:disabled="reqState === BillingInfoReqState.Sending"
>
</form>
</div>
<br>
{% if billing_infos_state == BillingInfoState.EMPTY %}
<div class="alert alert-yellow">
{% trans %}You must fill your billing infos if you want to pay with your credit
card{% endtrans %}
{% trans trimmed %}
You must fill your billing infos if you want to pay with your credit card
{% endtrans %}
</div>
{% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %}
<div class="alert alert-yellow">
{% trans %}
{% trans trimmed %}
The Crédit Agricole changed its policy related to the billing
information that must be provided in order to pay with a credit card.
If you want to pay with your credit card, you must add a phone number
@ -112,8 +113,14 @@
{% endtrans %}
</div>
{% endif %}
<form method="post" action="{{ settings.SITH_EBOUTIC_ET_URL }}" name="bank-pay-form">
<template x-data x-for="[key, value] in Object.entries($store.billing_inputs.data)">
<form
method="post"
action="{{ settings.SITH_EBOUTIC_ET_URL }}"
name="bank-pay-form"
x-data="etransactionData(initialEtData)"
@billing-infos-filled.window="await fill()"
>
<template x-for="[key, value] in Object.entries(data)" :key="key">
<input type="hidden" :name="key" :value="value">
</template>
<input
@ -140,17 +147,11 @@
{% block script %}
<script>
const billingInfoUrl = '{{ url("api:put_billing_info", user_id=request.user.id) }}';
const etDataUrl = '{{ url("api:etransaction_data") }}';
const billingInfoExist = {{ "true" if billing_infos else "false" }};
const billingInfoSuccessMessage = "{% trans %}Billing info registration success{% endtrans %}";
const billingInfoFailureMessage = "{% trans %}Billing info registration failure{% endtrans %}";
{% if billing_infos %}
const etData = {{ billing_infos|safe }}
{% else %}
const etData = {}
{% endif %}
{% if billing_infos -%}
const initialEtData = {{ billing_infos|safe }}
{%- else -%}
const initialEtData = {}
{%- endif %}
</script>
{{ super() }}
{% endblock %}

View File

@ -26,7 +26,9 @@ from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
LoginRequiredMixin,
)
from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, transaction
from django.http import HttpRequest, HttpResponse