Sauf erreur de notre part, nous avons bien reçu %s pour la saison %s, mais celle-ci n'a pas encore été finalisée au sein de notre club.
+
+
+ Pour finaliser l'inscription : Nous vous invitons à vous présenter directement au secrétariat de l'AS Talange lors de nos permanences pour valider l'inscription.
+
+
+
Nous restant à votre entière disposition pour tout renseignement complémentaire.
Nous vous contactons concernant %s (saison %s, catégorie %s) pour laquelle un solde reste actuellement à régler.
+
+
+
Situation Financière
+
+
+
Adhérent :
+
%s
+
+
+
Montant total cotisation :
+
%s
+
+
+
Total versé à ce jour :
+
%s
+
+
+
Solde restant à régler :
+
%s
+
+
+
+
+
+ Important :
+ Conformément aux statuts et au règlement intérieur de l'AS Talange, tant que le paiement de la cotisation n'est pas intégralement finalisé, l'inscription et la licence ne seront pas valides. L'accès aux entraînements et aux rencontres officielles pourra être suspendu.
+
+
+
Nous vous demandons de bien vouloir vous présenter au secrétariat du club lors des permanences afin d'effectuer le règlement du solde restant (%s).
+
+
Comptant sur votre prompt réajustement, Sportivement, Le Bureau de l'AS Talange
+
+
+
+
+
+ """.formatted(greeting, termeSujet, saisonNom, categorieNom, adherentNomComplet, prixTotalStr, totalPayeStr, resteStr, resteStr);
+ }
+}
diff --git a/as-talange-core/src/test/java/com/astalange/core/service/RelanceEmailServiceTest.java b/as-talange-core/src/test/java/com/astalange/core/service/RelanceEmailServiceTest.java
new file mode 100644
index 0000000..5b6b529
--- /dev/null
+++ b/as-talange-core/src/test/java/com/astalange/core/service/RelanceEmailServiceTest.java
@@ -0,0 +1,151 @@
+package com.astalange.core.service;
+
+import com.astalange.core.entity.Adherent;
+import com.astalange.core.entity.Categorie;
+import com.astalange.core.entity.Licence;
+import com.astalange.core.entity.PreInscription;
+import com.astalange.core.entity.Saison;
+import jakarta.mail.internet.MimeMessage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.mail.javamail.JavaMailSender;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class RelanceEmailServiceTest {
+
+ private RelanceEmailService relanceEmailService;
+ private JavaMailSender mailSenderMock;
+
+ @BeforeEach
+ void setUp() {
+ mailSenderMock = mock(JavaMailSender.class);
+ MimeMessage mimeMessage = new MimeMessage((jakarta.mail.Session) null);
+ when(mailSenderMock.createMimeMessage()).thenReturn(mimeMessage);
+ relanceEmailService = new RelanceEmailService(mailSenderMock);
+ }
+
+ @Test
+ void testIsMineur() {
+ LocalDate minorDob = LocalDate.now().minusYears(10);
+ LocalDate adultDob = LocalDate.now().minusYears(20);
+
+ assertTrue(relanceEmailService.isMineur(minorDob));
+ assertFalse(relanceEmailService.isMineur(adultDob));
+ assertFalse(relanceEmailService.isMineur(null));
+ }
+
+ @Test
+ void testSendRelancePreInscription_Mineur() {
+ PreInscription pre = new PreInscription();
+ pre.setId(1L);
+ pre.setNom("MARTIN");
+ pre.setPrenom("Lucas");
+ pre.setEmail("parent.martin@example.com");
+ pre.setDateNaissance(LocalDate.now().minusYears(12));
+ pre.setRepresentantLegal("Marc MARTIN");
+
+ Saison saison = new Saison();
+ saison.setNom("2026/2027");
+ pre.setSaison(saison);
+
+ boolean sent = relanceEmailService.sendRelancePreInscription(pre);
+ assertTrue(sent);
+
+ verify(mailSenderMock, timeout(2000).times(1)).send(any(MimeMessage.class));
+ }
+
+ @Test
+ void testSendRelancePreInscription_Majeur() {
+ PreInscription pre = new PreInscription();
+ pre.setId(2L);
+ pre.setNom("DUBOIS");
+ pre.setPrenom("Alexandre");
+ pre.setEmail("alex.dubois@example.com");
+ pre.setDateNaissance(LocalDate.now().minusYears(25));
+
+ Saison saison = new Saison();
+ saison.setNom("2026/2027");
+ pre.setSaison(saison);
+
+ boolean sent = relanceEmailService.sendRelancePreInscription(pre);
+ assertTrue(sent);
+
+ verify(mailSenderMock, timeout(2000).times(1)).send(any(MimeMessage.class));
+ }
+
+ @Test
+ void testSendRelancePreInscription_SansEmail() {
+ PreInscription pre = new PreInscription();
+ pre.setId(3L);
+ pre.setEmail(null);
+
+ boolean sent = relanceEmailService.sendRelancePreInscription(pre);
+ assertFalse(sent);
+ verify(mailSenderMock, never()).send(any(MimeMessage.class));
+ }
+
+ @Test
+ void testSendRelancePaiementCotisation_MineurAvecReste() {
+ Adherent adherent = new Adherent();
+ adherent.setId(10L);
+ adherent.setNom("GARCIA");
+ adherent.setPrenom("Leo");
+ adherent.setEmail("parent.garcia@example.com");
+ adherent.setDateNaissance(LocalDate.now().minusYears(14));
+ adherent.setRepresentantLegal("Sophie GARCIA");
+
+ Categorie cat = new Categorie();
+ cat.setNom("U15");
+ cat.setTarifBase(new BigDecimal("180.00"));
+ cat.setTarifExterieur(new BigDecimal("180.00"));
+
+ Saison saison = new Saison();
+ saison.setNom("2026/2027");
+
+ Licence licence = new Licence();
+ licence.setId(100L);
+ licence.setAdherent(adherent);
+ licence.setCategorie(cat);
+ licence.setSaison(saison);
+
+ boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
+ assertTrue(sent);
+
+ verify(mailSenderMock, timeout(2000).times(1)).send(any(MimeMessage.class));
+ }
+
+ @Test
+ void testSendRelancePaiementCotisation_SoldeZero() {
+ Adherent adherent = new Adherent();
+ adherent.setId(11L);
+ adherent.setNom("ROUX");
+ adherent.setPrenom("Thomas");
+ adherent.setEmail("thomas.roux@example.com");
+ adherent.setDateNaissance(LocalDate.now().minusYears(22));
+
+ Categorie cat = new Categorie();
+ cat.setNom("Senior");
+ cat.setTarifBase(new BigDecimal("200.00"));
+ cat.setTarifExterieur(new BigDecimal("200.00"));
+
+ Licence licence = new Licence();
+ licence.setId(101L);
+ licence.setAdherent(adherent);
+ licence.setCategorie(cat);
+
+ // Simulation d'un paiement intégral
+ com.astalange.core.entity.Paiement p = new com.astalange.core.entity.Paiement();
+ p.setMontant(new BigDecimal("200.00"));
+ licence.addPaiement(p);
+
+ boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
+ assertFalse(sent, "Aucune relance ne doit être envoyée si le reste à payer est égal à 0");
+ verify(mailSenderMock, never()).send(any(MimeMessage.class));
+ }
+}
diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java
index 62bd049..3ca7c40 100644
--- a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java
+++ b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java
@@ -25,8 +25,9 @@ public class AdherentController {
private final SaisonRepository saisonRepository;
private final com.astalange.core.service.CategorieService categorieService;
private final com.astalange.core.service.SportEasyEmailService sportEasyEmailService;
+ private final com.astalange.core.service.RelanceEmailService relanceEmailService;
- public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository, com.astalange.core.service.CategorieService categorieService, com.astalange.core.service.SportEasyEmailService sportEasyEmailService) {
+ public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository, com.astalange.core.service.CategorieService categorieService, com.astalange.core.service.SportEasyEmailService sportEasyEmailService, com.astalange.core.service.RelanceEmailService relanceEmailService) {
this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository;
@@ -34,6 +35,7 @@ public class AdherentController {
this.saisonRepository = saisonRepository;
this.categorieService = categorieService;
this.sportEasyEmailService = sportEasyEmailService;
+ this.relanceEmailService = relanceEmailService;
}
@org.springframework.web.bind.annotation.ModelAttribute("equipes")
@@ -382,4 +384,54 @@ public class AdherentController {
return "redirect:/adherents";
}
+
+ @org.springframework.web.bind.annotation.PostMapping("/{id}/relancer-paiement")
+ public String relancerPaiement(
+ @org.springframework.web.bind.annotation.PathVariable Long id,
+ org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
+
+ Adherent adherent = adherentRepository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
+
+ Licence licence = adherent.getLicenceActuelle();
+ if (licence == null) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Cet adhérent n'a pas de licence enregistrée.");
+ return "redirect:/adherents";
+ }
+
+ if (licence.getResteAPayer().compareTo(java.math.BigDecimal.ZERO) <= 0) {
+ redirectAttributes.addFlashAttribute("infoMessage", "Cet adhérent est déjà à jour de sa cotisation.");
+ return "redirect:/adherents";
+ }
+
+ boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
+ if (sent) {
+ redirectAttributes.addFlashAttribute("successMessage", "L'e-mail de relance de paiement pour " + adherent.getPrenom() + " " + adherent.getNom() + " a été envoyé avec succès.");
+ } else {
+ redirectAttributes.addFlashAttribute("errorMessage", "Impossible d'envoyer l'e-mail de relance (adresse e-mail manquante).");
+ }
+
+ return "redirect:/adherents";
+ }
+
+ @org.springframework.web.bind.annotation.PostMapping("/{id}/relancer-paiement-htmx")
+ @org.springframework.web.bind.annotation.ResponseBody
+ public String relancerPaiementHtmx(@org.springframework.web.bind.annotation.PathVariable Long id) {
+ Adherent adherent = adherentRepository.findById(id).orElse(null);
+ if (adherent == null || adherent.getLicenceActuelle() == null) {
+ return "Introuvable";
+ }
+
+ Licence licence = adherent.getLicenceActuelle();
+ if (licence.getResteAPayer().compareTo(java.math.BigDecimal.ZERO) <= 0) {
+ return "Déjà réglé";
+ }
+
+ boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
+ if (sent) {
+ return " Relance envoyée";
+ } else {
+ return "Email manquant";
+ }
+ }
}
diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/AdminPreInscriptionController.java b/as-talange-web/src/main/java/com/astalange/web/controller/AdminPreInscriptionController.java
index b21d309..4297dc7 100644
--- a/as-talange-web/src/main/java/com/astalange/web/controller/AdminPreInscriptionController.java
+++ b/as-talange-web/src/main/java/com/astalange/web/controller/AdminPreInscriptionController.java
@@ -8,17 +8,23 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
+import com.astalange.core.entity.PreInscription;
+import com.astalange.core.service.RelanceEmailService;
+
@Controller
@RequestMapping("/admin/pre-inscriptions")
public class AdminPreInscriptionController {
private final PreInscriptionRepository preInscriptionRepository;
private final PreInscriptionService preInscriptionService;
+ private final RelanceEmailService relanceEmailService;
public AdminPreInscriptionController(PreInscriptionRepository preInscriptionRepository,
- PreInscriptionService preInscriptionService) {
+ PreInscriptionService preInscriptionService,
+ RelanceEmailService relanceEmailService) {
this.preInscriptionRepository = preInscriptionRepository;
this.preInscriptionService = preInscriptionService;
+ this.relanceEmailService = relanceEmailService;
}
@GetMapping
@@ -46,6 +52,20 @@ public class AdminPreInscriptionController {
return ""; // HTMX target la ligne avec outerHTML -> supprime la ligne
}
+ @PostMapping("/{id}/relancer")
+ @ResponseBody
+ public String relancer(@PathVariable Long id) {
+ PreInscription pre = preInscriptionRepository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("Pré-inscription introuvable : " + id));
+
+ boolean sent = relanceEmailService.sendRelancePreInscription(pre);
+ if (sent) {
+ return " Relance envoyée";
+ } else {
+ return "Erreur (sans email)";
+ }
+ }
+
@GetMapping("/count")
@ResponseBody
public String countBadge() {
diff --git a/as-talange-web/src/main/resources/templates/adherents/form.html b/as-talange-web/src/main/resources/templates/adherents/form.html
index 6bf08bc..d197332 100644
--- a/as-talange-web/src/main/resources/templates/adherents/form.html
+++ b/as-talange-web/src/main/resources/templates/adherents/form.html
@@ -10,6 +10,8 @@
body { font-family: 'Inter', sans-serif; }
.hidden-block { display: none; }
+
+
@@ -306,6 +308,15 @@
th:data-reste-a-payer="${lic.getResteAPayer()}"
onclick="openPaiementModal(this.getAttribute('data-licence-id'), this.getAttribute('data-reste-a-payer'))"
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg btn-payer">Payer
+
+
+