fix(licence): corriger l'évaluation SpEL Thymeleaf et finaliser la réduction éducateur
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 5m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 6m47s

This commit is contained in:
2026-08-07 12:11:06 +02:00
parent 5875a66c3d
commit 14730e667f
6 changed files with 242 additions and 26 deletions
@@ -50,15 +50,21 @@ public class Licence {
@Column(name = "sport_easy_email_sent_at")
private java.time.LocalDateTime sportEasyEmailSentAt;
@Column(name = "reduction_educateur", nullable = false)
private Boolean reductionEducateur = false;
@Column(name = "pourcentage_reduction_educateur")
private Integer pourcentageReductionEducateur = 100;
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Paiement> paiements = new ArrayList<>();
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Dotation> dotations = new ArrayList<>();
// Logic for Global Total Price
// Logic for Raw Price (before reduction)
@Transient
public BigDecimal getPrixTotal() {
public BigDecimal getPrixBrut() {
if (categorie == null) return BigDecimal.ZERO;
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
@@ -69,7 +75,7 @@ public class Licence {
if (dotations != null) {
for (Dotation dot : dotations) {
if (dot.getChoisi() && dot.getEquipement() != null) {
if (dot != null && Boolean.TRUE.equals(dot.getChoisi()) && dot.getEquipement() != null) {
BigDecimal price = categorie.getEquipementPrix(dot.getEquipement().getId());
if (price != null) {
prixTotal = prixTotal.add(price);
@@ -81,6 +87,50 @@ public class Licence {
return prixTotal;
}
@Transient
public BigDecimal getEquipementsContrib() {
if (categorie == null) return BigDecimal.ZERO;
BigDecimal tarifCategorie = (adherent != null && adherent.isResidentTalange())
? categorie.getTarifBase()
: categorie.getTarifExterieur();
if (tarifCategorie == null) tarifCategorie = BigDecimal.ZERO;
BigDecimal prixBrut = getPrixBrut();
BigDecimal contrib = prixBrut.subtract(tarifCategorie);
return contrib.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : contrib;
}
// Logic for Global Total Price (after discount if applicable)
@Transient
public BigDecimal getPrixTotal() {
BigDecimal prixBrut = getPrixBrut();
if (Boolean.TRUE.equals(reductionEducateur)) {
int pct = (pourcentageReductionEducateur != null && pourcentageReductionEducateur >= 1 && pourcentageReductionEducateur <= 100)
? pourcentageReductionEducateur
: 100;
if (pct >= 100) {
return BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
} else {
BigDecimal multiplier = BigDecimal.valueOf(100 - pct)
.divide(BigDecimal.valueOf(100), 4, java.math.RoundingMode.HALF_UP);
return prixBrut.multiply(multiplier).setScale(2, java.math.RoundingMode.HALF_UP);
}
}
return prixBrut.setScale(2, java.math.RoundingMode.HALF_UP);
}
@Transient
public BigDecimal getMontantReduction() {
if (!Boolean.TRUE.equals(reductionEducateur)) {
return BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
}
BigDecimal prixBrut = getPrixBrut();
BigDecimal prixTotal = getPrixTotal();
return prixBrut.subtract(prixTotal).setScale(2, java.math.RoundingMode.HALF_UP);
}
// Logic for Reste A Payer
@Transient
public BigDecimal getResteAPayer() {
@@ -146,6 +196,12 @@ public class Licence {
public java.time.LocalDateTime getSportEasyEmailSentAt() { return sportEasyEmailSentAt; }
public void setSportEasyEmailSentAt(java.time.LocalDateTime sportEasyEmailSentAt) { this.sportEasyEmailSentAt = sportEasyEmailSentAt; }
public Boolean getReductionEducateur() { return reductionEducateur != null ? reductionEducateur : false; }
public void setReductionEducateur(Boolean reductionEducateur) { this.reductionEducateur = reductionEducateur; }
public Integer getPourcentageReductionEducateur() { return pourcentageReductionEducateur != null ? pourcentageReductionEducateur : 100; }
public void setPourcentageReductionEducateur(Integer pourcentageReductionEducateur) { this.pourcentageReductionEducateur = pourcentageReductionEducateur; }
@Transient
public boolean isRenouvellement() {
return typeDemande != null && (
@@ -0,0 +1,2 @@
ALTER TABLE licence ADD COLUMN reduction_educateur BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE licence ADD COLUMN pourcentage_reduction_educateur INT DEFAULT 100;
@@ -0,0 +1,98 @@
package com.astalange.core;
import com.astalange.core.entity.Adherent;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Licence;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
class LicenceReductionTest {
@Test
@DisplayName("Devrait calculer le prix normal sans réduction")
void testPrixSansReduction() {
Categorie cat = new Categorie();
cat.setTarifBase(new BigDecimal("100.00"));
cat.setTarifExterieur(new BigDecimal("120.00"));
Adherent adh = new Adherent();
adh.setResidentTalange(true);
Licence licence = new Licence();
licence.setCategorie(cat);
licence.setAdherent(adh);
licence.setReductionEducateur(false);
assertEquals(new BigDecimal("100.00"), licence.getPrixBrut());
assertEquals(new BigDecimal("100.00"), licence.getPrixTotal());
assertEquals(new BigDecimal("0.00"), licence.getMontantReduction());
assertEquals(new BigDecimal("100.00"), licence.getResteAPayer());
}
@Test
@DisplayName("Devrait calculer une réduction de 100% (gratuit)")
void testReduction100Pourcent() {
Categorie cat = new Categorie();
cat.setTarifBase(new BigDecimal("140.00"));
Adherent adh = new Adherent();
adh.setResidentTalange(true);
Licence licence = new Licence();
licence.setCategorie(cat);
licence.setAdherent(adh);
licence.setReductionEducateur(true);
licence.setPourcentageReductionEducateur(100);
assertEquals(new BigDecimal("140.00"), licence.getPrixBrut());
assertEquals(new BigDecimal("0.00"), licence.getPrixTotal());
assertEquals(new BigDecimal("140.00"), licence.getMontantReduction());
assertEquals(new BigDecimal("0.00"), licence.getResteAPayer());
}
@Test
@DisplayName("Devrait calculer une réduction partielle (ex: 50%)")
void testReductionPartielle50Pourcent() {
Categorie cat = new Categorie();
cat.setTarifBase(new BigDecimal("150.00"));
Adherent adh = new Adherent();
adh.setResidentTalange(true);
Licence licence = new Licence();
licence.setCategorie(cat);
licence.setAdherent(adh);
licence.setReductionEducateur(true);
licence.setPourcentageReductionEducateur(50);
assertEquals(new BigDecimal("150.00"), licence.getPrixBrut());
assertEquals(new BigDecimal("75.00"), licence.getPrixTotal());
assertEquals(new BigDecimal("75.00"), licence.getMontantReduction());
assertEquals(new BigDecimal("75.00"), licence.getResteAPayer());
}
@Test
@DisplayName("Devrait calculer une réduction partielle de 20%")
void testReductionPartielle20Pourcent() {
Categorie cat = new Categorie();
cat.setTarifBase(new BigDecimal("200.00"));
Adherent adh = new Adherent();
adh.setResidentTalange(true);
Licence licence = new Licence();
licence.setCategorie(cat);
licence.setAdherent(adh);
licence.setReductionEducateur(true);
licence.setPourcentageReductionEducateur(20);
assertEquals(new BigDecimal("200.00"), licence.getPrixBrut());
assertEquals(new BigDecimal("160.00"), licence.getPrixTotal());
assertEquals(new BigDecimal("40.00"), licence.getMontantReduction());
assertEquals(new BigDecimal("160.00"), licence.getResteAPayer());
}
}
@@ -50,7 +50,9 @@ public class LicenceController {
@RequestParam(required = false) String typeDemande,
@RequestParam(required = false) String typeLicence,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) Long equipeId) {
@RequestParam(required = false) Long equipeId,
@RequestParam(required = false, defaultValue = "false") Boolean reductionEducateur,
@RequestParam(required = false, defaultValue = "100") Integer pourcentageReductionEducateur) {
Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Adherent ID"));
@@ -75,6 +77,8 @@ public class LicenceController {
licence.setTypeDemande(typeDemande);
licence.setTypeLicence(typeLicence);
licence.setCommentaire(commentaire);
licence.setReductionEducateur(Boolean.TRUE.equals(reductionEducateur));
licence.setPourcentageReductionEducateur(pourcentageReductionEducateur != null ? pourcentageReductionEducateur : 100);
if (equipeId != null) {
Equipe equipe = equipeRepository.findById(equipeId)
@@ -100,7 +104,9 @@ public class LicenceController {
@RequestParam(required = false) String typeDemande,
@RequestParam(required = false) String typeLicence,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) Long equipeId) {
@RequestParam(required = false) Long equipeId,
@RequestParam(required = false, defaultValue = "false") Boolean reductionEducateur,
@RequestParam(required = false, defaultValue = "100") Integer pourcentageReductionEducateur) {
Licence licence = licenceRepository.findById(licenceId)
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
@@ -114,6 +120,8 @@ public class LicenceController {
licence.setTypeDemande(typeDemande);
licence.setTypeLicence(typeLicence);
licence.setCommentaire(commentaire);
licence.setReductionEducateur(Boolean.TRUE.equals(reductionEducateur));
licence.setPourcentageReductionEducateur(pourcentageReductionEducateur != null ? pourcentageReductionEducateur : 100);
if (equipeId != null) {
Equipe equipe = equipeRepository.findById(equipeId)
@@ -189,7 +197,7 @@ public class LicenceController {
StringBuilder sb = new StringBuilder();
sb.append('\ufeff');
sb.append("Nom;Prénom;Catégorie;N° Licence;Email;Type de Demande;Saison;Etat\n");
sb.append("Nom;Prénom;Catégorie;N° Licence;Email;Type de Demande;Réduction Éducateur;Saison;Etat\n");
for (Licence l : licences) {
String n = l.getAdherent() != null && l.getAdherent().getNom() != null ? l.getAdherent().getNom() : "";
@@ -198,6 +206,7 @@ public class LicenceController {
String num = l.getNumeroLicence() != null ? l.getNumeroLicence() : "";
String e = l.getAdherent() != null && l.getAdherent().getEmail() != null ? l.getAdherent().getEmail() : "";
String td = l.getTypeDemande() != null ? l.getTypeDemande() : "";
String red = Boolean.TRUE.equals(l.getReductionEducateur()) ? ("-" + l.getPourcentageReductionEducateur() + "%") : "Non";
String s = l.getSaison() != null ? l.getSaison().getNom() : "";
String etat = l.getEtat() != null ? l.getEtat() : "";
@@ -207,6 +216,7 @@ public class LicenceController {
.append(escapeCsv(num)).append(";")
.append(escapeCsv(e)).append(";")
.append(escapeCsv(td)).append(";")
.append(escapeCsv(red)).append(";")
.append(escapeCsv(s)).append(";")
.append(escapeCsv(etat)).append("\n");
}
@@ -226,7 +226,7 @@
<div th:if="${adherent.id != null}" class="max-w-6xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3>
<button th:if="${#lists.isEmpty(licences)}" onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
<button onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
+ Ajouter une Licence
</button>
</div>
@@ -251,14 +251,14 @@
</tr>
<th:block th:each="lic : ${licences}">
<tr class="hover:bg-gray-50 border-b border-gray-100 licence-row"
th:data-tarif-base="${lic.categorie.tarifBase}"
th:data-tarif-exterieur="${lic.categorie.tarifExterieur}"
th:data-equipements-contrib="${lic.getPrixTotal().subtract(adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur)}"
th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}">
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison.nom}">2024-2025</td>
th:data-tarif-base="${lic.categorie != null ? lic.categorie.tarifBase : 0}"
th:data-tarif-exterieur="${lic.categorie != null ? lic.categorie.tarifExterieur : 0}"
th:data-equipements-contrib="${lic.getEquipementsContrib()}"
th:data-total-paye="${lic.getSommePayee()}">
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison != null ? lic.saison.nom : '-'}">2024-2025</td>
<td class="py-4 px-4 text-gray-600">
<span th:text="${lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.nom + ')' : '')}">U15</span>
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
<span th:text="${lic.categorie != null ? (lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.nom + ')' : '')) : '-'}">U15</span>
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${lic.categorie != null ? ((lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)') : '-'}">100.00 € (Catégorie)</span>
</td>
<td class="py-4 px-4 text-gray-600 text-xs">
<div th:text="${lic.typeDemande != null ? lic.typeDemande : '-'}">Nouvelle</div>
@@ -270,23 +270,37 @@
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
th:text="${lic.etat}">Brouillon</span>
</td>
<td class="py-4 px-4 text-gray-900 font-medium cell-tarif-global" th:text="${lic.getPrixTotal() + ' €'}">130.00 €</td>
<td class="py-4 px-4 font-medium cell-tarif-global">
<div th:if="${lic.reductionEducateur}" class="flex flex-col">
<span class="text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-200 px-2 py-0.5 rounded-full inline-block w-max mb-0.5"
th:text="'Éducateur (-' + ${lic.pourcentageReductionEducateur != null ? lic.pourcentageReductionEducateur : 100} + '%)'">Éducateur (-100%)</span>
<div class="flex items-center space-x-1 text-xs">
<span th:if="${lic.getPrixBrut().compareTo(lic.getPrixTotal()) != 0}" class="line-through text-gray-400" th:text="${lic.getPrixBrut() + ' €'}">150.00 €</span>
<span class="text-gray-900 font-bold" th:text="${lic.getPrixTotal() + ' €'}">0.00 €</span>
</div>
</div>
<div th:if="${!lic.reductionEducateur}">
<span class="text-gray-900" th:text="${lic.getPrixTotal() + ' €'}">130.00 €</span>
</div>
</td>
<td class="py-4 px-4 font-semibold cell-reste-payer"
th:classappend="${lic.getResteAPayer() == 0 ? 'text-green-600' : 'text-red-600'}"
th:classappend="${lic.getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'text-green-600' : 'text-red-600'}"
th:text="${lic.getResteAPayer() + ' €'}">30.00 €</td>
<td class="py-4 px-4 text-right flex justify-end space-x-2">
<button type="button"
th:data-licence-id="${lic.id}"
th:data-categorie-id="${lic.categorie.id}"
th:data-numero-licence="${lic.numeroLicence}"
th:data-etat="${lic.etat}"
th:data-type-demande="${lic.typeDemande}"
th:data-type-licence="${lic.typeLicence}"
th:data-categorie-id="${lic.categorie != null ? lic.categorie.id : ''}"
th:data-numero-licence="${lic.numeroLicence != null ? lic.numeroLicence : ''}"
th:data-etat="${lic.etat != null ? lic.etat : 'Brouillon'}"
th:data-type-demande="${lic.typeDemande != null ? lic.typeDemande : ''}"
th:data-type-licence="${lic.typeLicence != null ? lic.typeLicence : ''}"
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
th:data-commentaire="${lic.commentaire}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-type-demande'), this.getAttribute('data-type-licence'), this.getAttribute('data-equipe-id'), this.getAttribute('data-commentaire'))"
th:data-commentaire="${lic.commentaire != null ? lic.commentaire : ''}"
th:data-reduction-educateur="${lic.reductionEducateur}"
th:data-pourcentage-reduction="${lic.pourcentageReductionEducateur != null ? lic.pourcentageReductionEducateur : 100}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-type-demande'), this.getAttribute('data-type-licence'), this.getAttribute('data-equipe-id'), this.getAttribute('data-commentaire'), this.getAttribute('data-reduction-educateur'), this.getAttribute('data-pourcentage-reduction'))"
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
<button th:if="${lic.getResteAPayer() > 0}"
<button th:if="${lic.getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) > 0}"
type="button"
th:data-licence-id="${lic.id}"
th:data-reste-a-payer="${lic.getResteAPayer()}"
@@ -554,6 +568,19 @@
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire</label>
<textarea id="licenceCommentaireInput" name="commentaire" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Commentaire optionnel..."></textarea>
</div>
<div class="bg-amber-50 p-3 rounded-lg border border-amber-200">
<label class="flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" id="reductionEducateurInput" name="reductionEducateur" value="true" onchange="toggleReductionPourcentageVisibility()" class="w-4 h-4 text-amber-600 border-gray-300 rounded focus:ring-amber-500">
<span class="text-sm font-semibold text-amber-900">Enfant d'éducateur / Réduction</span>
</label>
<div id="pourcentageReductionBlock" class="hidden mt-2 pt-2 border-t border-amber-200/60">
<label class="block text-xs font-medium text-amber-800 mb-1">Taux de réduction (%)</label>
<div class="flex items-center space-x-2">
<input type="number" id="pourcentageReductionInput" name="pourcentageReductionEducateur" min="1" max="100" value="100" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-amber-500 outline-none">
<span class="text-xs font-bold text-amber-800">%</span>
</div>
</div>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
@@ -644,7 +671,17 @@
</div>
<script th:inline="javascript">
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', typeDemande = '', typeLicence = '', equipeId = '', commentaire = '') {
function toggleReductionPourcentageVisibility() {
const chk = document.getElementById('reductionEducateurInput');
const block = document.getElementById('pourcentageReductionBlock');
if (chk && chk.checked) {
block.classList.remove('hidden');
} else if (block) {
block.classList.add('hidden');
}
}
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', typeDemande = '', typeLicence = '', equipeId = '', commentaire = '', reductionEducateur = false, pourcentageReductionEducateur = 100) {
document.getElementById('licenceModal').classList.remove('hidden');
const form = document.getElementById('licenceForm');
const title = document.getElementById('licenceModalTitle');
@@ -655,6 +692,16 @@
// Clean up team options first
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
const redChk = document.getElementById('reductionEducateurInput');
const pctInput = document.getElementById('pourcentageReductionInput');
if (redChk) {
redChk.checked = (reductionEducateur === true || reductionEducateur === 'true');
}
if (pctInput) {
pctInput.value = (pourcentageReductionEducateur !== null && pourcentageReductionEducateur !== undefined && pourcentageReductionEducateur !== '') ? pourcentageReductionEducateur : 100;
}
toggleReductionPourcentageVisibility();
if (licenceId) {
title.textContent = 'Modifier la Licence';
form.action = '/adherents/' + adherentId + '/licences/' + licenceId + '/update';
@@ -86,7 +86,10 @@
</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.categorie != null ? licence.categorie.nom : '-'}">Catégorie</td>
<td class="py-3 px-4 text-gray-600 font-mono" th:text="${licence.numeroLicence != null ? licence.numeroLicence : '-'}"></td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.typeDemande != null ? licence.typeDemande : '-'}">Type</td>
<td class="py-3 px-4 text-gray-600">
<div th:text="${licence.typeDemande != null ? licence.typeDemande : '-'}">Type</div>
<span th:if="${licence.reductionEducateur}" class="text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded-full inline-block mt-0.5" th:text="'Éducateur (-' + ${licence.pourcentageReductionEducateur} + '%)'">Éducateur (-100%)</span>
</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.adherent.email != null ? licence.adherent.email : '-'}">Email</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.etat != null ? licence.etat : '-'}">Etat</td>
<td class="py-3 px-4 text-right">