fix: recalculate member category and tariffs dynamically on birthdate change

This commit is contained in:
2026-05-30 00:14:02 +02:00
parent 0baca4a334
commit 3ae296aa91
3 changed files with 189 additions and 2 deletions
+6
View File
@@ -61,5 +61,11 @@
<version>1.77</version> <version>1.77</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -0,0 +1,93 @@
package com.astalange.core;
import com.astalange.core.entity.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AdherentRecalculationTest {
@Test
public void testLicencePriceCalculationBasedOnResidencyAndCategory() {
Adherent adherent = new Adherent();
adherent.setNom("Doe");
adherent.setPrenom("John");
adherent.setDateNaissance(LocalDate.of(2015, 5, 10)); // born 2015
adherent.setResidentTalange(true);
Categorie u11 = new Categorie();
u11.setNom("U11");
u11.setAnneeMin(2015);
u11.setAnneeMax(2015);
u11.setTarifBase(new BigDecimal("130.00"));
u11.setTarifExterieur(new BigDecimal("160.00"));
Licence licence = new Licence();
licence.setAdherent(adherent);
licence.setCategorie(u11);
// Under residentTalange = true, base price should be 130
assertEquals(new BigDecimal("130.00"), licence.getPrixTotal());
// Under residentTalange = false, base price should be 160
adherent.setResidentTalange(false);
assertEquals(new BigDecimal("160.00"), licence.getPrixTotal());
}
@Test
public void testCategoryUpdateOnBirthdateChangeSimulation() {
// Set up adherent
Adherent adherent = new Adherent();
adherent.setDateNaissance(LocalDate.of(2015, 5, 10)); // 2015 birth year
Saison saison = new Saison();
saison.setNom("2025-2026");
saison.setEstActive(true);
// Set up categories
Categorie u11 = new Categorie();
u11.setNom("U11");
u11.setAnneeMin(2015);
u11.setAnneeMax(2015);
u11.setSaison(saison);
u11.setTarifBase(new BigDecimal("130.00"));
Categorie u12 = new Categorie();
u12.setNom("U12");
u12.setAnneeMin(2014);
u12.setAnneeMax(2014);
u12.setSaison(saison);
u12.setTarifBase(new BigDecimal("140.00"));
List<Categorie> categories = List.of(u11, u12);
// Licence initially created for U11
Licence licence = new Licence();
licence.setAdherent(adherent);
licence.setCategorie(u11);
licence.setSaison(saison);
// Simulation of date of birth changing to 2014 (U12)
adherent.setDateNaissance(LocalDate.of(2014, 5, 10));
int newBirthYear = adherent.getDateNaissance().getYear();
// Simulate Controller's update logic
Categorie newCat = categories.stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
assertNotNull(newCat);
assertEquals("U12", newCat.getNom());
licence.setCategorie(newCat);
// Verify the licence's category has updated to U12
assertEquals("U12", licence.getCategorie().getNom());
// Verify price total is updated dynamically
assertEquals(new BigDecimal("140.00"), licence.getPrixTotal());
}
}
@@ -322,8 +322,11 @@
<select id="categorieSelect" name="categorieId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <select id="categorieSelect" name="categorieId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez une catégorie...</option> <option value="">Sélectionnez une catégorie...</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" <option th:each="cat : ${categories}" th:value="${cat.id}"
th:data-nom="${cat.nom}"
th:data-annee-min="${cat.anneeMin}" th:data-annee-min="${cat.anneeMin}"
th:data-annee-max="${cat.anneeMax}" th:data-annee-max="${cat.anneeMax}"
th:data-tarif-base="${cat.tarifBase}"
th:data-tarif-exterieur="${cat.tarifExterieur}"
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option> th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
</select> </select>
</div> </div>
@@ -443,6 +446,7 @@
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const dateInput = document.getElementById('dateNaissance'); const dateInput = document.getElementById('dateNaissance');
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
const residentCheckbox = document.getElementById('residentTalange');
function calculateAge(birthday) { function calculateAge(birthday) {
const ageDifMs = Date.now() - birthday.getTime(); const ageDifMs = Date.now() - birthday.getTime();
@@ -464,7 +468,92 @@
} }
} }
dateInput.addEventListener('change', updateRepresentantBlock); function updateLicencesCategoryAndPrices() {
if (!dateInput.value) return;
const dob = new Date(dateInput.value);
const birthYear = dob.getFullYear();
const isResident = residentCheckbox ? residentCheckbox.checked : true;
const categories = [];
const catSelect = document.getElementById('categorieSelect');
if (catSelect) {
Array.from(catSelect.options).forEach(opt => {
const id = opt.value;
const nom = opt.getAttribute('data-nom');
const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max'));
const tarifBase = parseFloat(opt.getAttribute('data-tarif-base') || '0');
const tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0');
if (id) {
categories.push({ id, nom, min, max, tarifBase, tarifExterieur });
}
});
}
if (categories.length === 0) return;
const matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max);
if (!matchingCat) return;
document.querySelectorAll('.licence-row').forEach(row => {
row.setAttribute('data-tarif-base', matchingCat.tarifBase);
row.setAttribute('data-tarif-exterieur', matchingCat.tarifExterieur);
const baseTarif = isResident ? matchingCat.tarifBase : matchingCat.tarifExterieur;
const equipContrib = parseFloat(row.getAttribute('data-equipements-contrib') || '0');
const totalPaye = parseFloat(row.getAttribute('data-total-paye') || '0');
const newTotal = baseTarif + equipContrib;
const newReste = Math.max(0, newTotal - totalPaye);
const catCell = row.querySelector('.cell-tarif-categorie');
if (catCell) {
const parentTd = catCell.parentNode;
const spanName = parentTd.querySelector('span:not(.cell-tarif-categorie)');
if (spanName) {
spanName.textContent = matchingCat.nom;
}
catCell.textContent = baseTarif.toFixed(2) + ' € (Catégorie)';
}
const globalCell = row.querySelector('.cell-tarif-global');
if (globalCell) {
globalCell.textContent = newTotal.toFixed(2) + ' €';
}
const resteCell = row.querySelector('.cell-reste-payer');
if (resteCell) {
resteCell.textContent = newReste.toFixed(2) + ' €';
if (newReste === 0) {
resteCell.classList.remove('text-red-600');
resteCell.classList.add('text-green-600');
} else {
resteCell.classList.remove('text-green-600');
resteCell.classList.add('text-red-600');
}
}
const payerBtn = row.querySelector('.btn-payer');
if (payerBtn) {
if (newReste > 0) {
payerBtn.classList.remove('hidden');
const licId = payerBtn.getAttribute('data-licence-id');
payerBtn.setAttribute('onclick', `openPaiementModal(${licId}, ${newReste})`);
} else {
payerBtn.classList.add('hidden');
}
}
const modifierBtn = row.querySelector('button[onclick^="openLicenceModal"]');
if (modifierBtn) {
modifierBtn.setAttribute('data-categorie-id', matchingCat.id);
}
});
}
dateInput.addEventListener('change', function() {
updateRepresentantBlock();
updateLicencesCategoryAndPrices();
});
// Initial check in case it's a validation error reload // Initial check in case it's a validation error reload
updateRepresentantBlock(); updateRepresentantBlock();
@@ -475,7 +564,6 @@
} }
// Recalculate price dynamically when residentTalange checkbox changes // Recalculate price dynamically when residentTalange checkbox changes
const residentCheckbox = document.getElementById('residentTalange');
if (residentCheckbox) { if (residentCheckbox) {
residentCheckbox.addEventListener('change', function() { residentCheckbox.addEventListener('change', function() {
const isResident = this.checked; const isResident = this.checked;