From 2a1bdfbdcbee1eda3961cb476c4a2d8bbf0b65e2 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 20 Aug 2025 21:59:26 +0200 Subject: [PATCH] feat: helloasso competition register --- .../data/model/HelloAssoRegisterModel.java | 41 ++++++ .../HelloAssoRegisterRepository.java | 9 ++ .../domain/service/CompetitionService.java | 121 +++++++++++++++++- .../ffsaf/domain/service/WebhookService.java | 18 ++- .../rest/client/dto/NotificationData.java | 35 +++++ .../ffsaf/rest/data/RegisterRequestData.java | 4 + 6 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/HelloAssoRegisterModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/HelloAssoRegisterRepository.java diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/HelloAssoRegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/HelloAssoRegisterModel.java new file mode 100644 index 0000000..da0515f --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/HelloAssoRegisterModel.java @@ -0,0 +1,41 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.data.id.RegisterId; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "helloasso_register") +public class HelloAssoRegisterModel { + @EmbeddedId + RegisterId id; + + @MapsId("competitionId") + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinColumn(name = "id_competition") + CompetitionModel competition; + + @MapsId("membreId") + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinColumn(name = "id_membre") + MembreModel membre; + + Integer orderId; + + public HelloAssoRegisterModel(CompetitionModel competition, MembreModel membre, Integer orderId) { + this.id = new RegisterId(competition.getId(), membre.getId()); + this.competition = competition; + this.membre = membre; + this.orderId = orderId; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/HelloAssoRegisterRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/HelloAssoRegisterRepository.java new file mode 100644 index 0000000..355275d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/HelloAssoRegisterRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.HelloAssoRegisterModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class HelloAssoRegisterRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index f01590a..f1350ab 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -1,6 +1,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.CompetitionModel; +import fr.titionfire.ffsaf.data.model.HelloAssoRegisterModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.RegisterModel; import fr.titionfire.ffsaf.data.repository.*; @@ -8,6 +9,7 @@ import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCompet; import fr.titionfire.ffsaf.net2.request.SReqCompet; import fr.titionfire.ffsaf.net2.request.SReqRegister; +import fr.titionfire.ffsaf.rest.client.dto.NotificationData; import fr.titionfire.ffsaf.rest.data.CompetitionData; import fr.titionfire.ffsaf.rest.data.RegisterRequestData; import fr.titionfire.ffsaf.rest.data.SimpleCompetData; @@ -22,13 +24,18 @@ import io.quarkus.cache.Cache; import io.quarkus.cache.CacheName; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.reactive.ReactiveMailer; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import io.vertx.mutiny.core.Vertx; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; import org.hibernate.reactive.mutiny.Mutiny; +import org.jboss.logging.Logger; import org.keycloak.representations.idm.UserRepresentation; import java.util.*; @@ -37,6 +44,7 @@ import java.util.stream.Stream; @WithSession @ApplicationScoped public class CompetitionService { + private static final Logger LOGGER = Logger.getLogger(CompetitionService.class); @Inject CompetitionRepository repository; @@ -65,6 +73,13 @@ public class CompetitionService { @Inject CompetPermService permService; + @Inject + HelloAssoRegisterRepository helloAssoRepository; + + @SuppressWarnings("CdiInjectionPointsInspection") + @Inject + ReactiveMailer reactiveMailer; + @Inject Vertx vertx; @@ -249,7 +264,7 @@ public class CompetitionService { if ("admin".equals(source)) return permService.hasEditPerm(securityCtx, id) .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) - .chain(combModel -> updateRegister(id, data, c, combModel, true))) + .chain(combModel -> updateRegister(data, c, combModel, true))) .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) .map(licences -> SimpleRegisterComb.fromModel(r, licences))); if ("club".equals(source)) @@ -269,7 +284,7 @@ public class CompetitionService { throw new DForbiddenException( "Vous n'avez pas le droit d'inscrire ce membre (par décision de l'administrateur de la compétition)"); })) - .chain(combModel -> updateRegister(id, data, c, combModel, false))) + .chain(combModel -> updateRegister(data, c, combModel, false))) .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) .map(licences -> SimpleRegisterComb.fromModel(r, licences))); @@ -286,13 +301,13 @@ public class CompetitionService { throw new DForbiddenException( "Vous n'avez pas le droit de vous inscrire (par décision de l'administrateur de la compétition)"); })) - .chain(combModel -> updateRegister(id, data, c, combModel, false))) + .chain(combModel -> updateRegister(data, c, combModel, false))) .map(r -> SimpleRegisterComb.fromModel(r, List.of())); } - private Uni updateRegister(Long id, RegisterRequestData data, CompetitionModel c, + private Uni updateRegister(RegisterRequestData data, CompetitionModel c, MembreModel combModel, boolean admin) { - return registerRepository.find("competition.id = ?1 AND membre = ?2", id, combModel).firstResult() + return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult() .onFailure().recoverWithNull() .map(Unchecked.function(r -> { if (r != null) { @@ -480,4 +495,100 @@ public class CompetitionService { })) .call(__ -> cache.invalidate(data.getId())); } + + public Uni unregisterHelloAsso(NotificationData data) { + if (!data.getState().equals("Refunded")) + return Uni.createFrom().item(Response.ok().build()); + + return helloAssoRepository.list("orderId = ?1", data.getOrder().getId()) + .chain(regs -> { + Uni uni = Uni.createFrom().nullItem(); + + for (HelloAssoRegisterModel reg : regs) { + if (reg.getCompetition().getRegisterMode() != RegisterMode.HELLOASSO) + continue; + if (!data.getOrder().getOrganizationSlug().equalsIgnoreCase(reg.getCompetition().getData1())) + continue; + + uni = uni.call(__ -> Panache.withTransaction( + () -> registerRepository.delete("competition = ?1 AND membre = ?2", + reg.getCompetition(), reg.getMembre()))); + } + + return uni; + }) + .onFailure().invoke(Throwable::printStackTrace) + .map(__ -> Response.ok().build()); + } + + public Uni registerHelloAsso(NotificationData data) { + String organizationSlug = data.getOrganizationSlug(); + String formSlug = data.getFormSlug(); + RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false); + + return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult() + .onFailure().recoverWithNull() + .chain(cm -> { + Uni uni = Uni.createFrom().nullItem(); + if (cm == null || cm.getRegisterMode() != RegisterMode.HELLOASSO) + return uni; + + List place = List.of(cm.getData3().toLowerCase().split(";")); + List fail = new ArrayList<>(); + + for (NotificationData.Item item : data.getItems()) { + if (!place.contains(item.getName().toLowerCase())) + continue; + if (item.getCustomFields() == null || item.getCustomFields().isEmpty()) { + fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(), + item.getUser().getFirstName())); + continue; + } + + Optional optional = item.getCustomFields().stream() + .filter(cf -> cf.getName().equalsIgnoreCase("Numéro de licence")).findAny().map( + NotificationData.CustomField::getAnswer).map(Long::valueOf); + + if (optional.isPresent()) { + uni = uni.call(__ -> membreService.getByLicence(optional.get()) + .invoke(Unchecked.consumer(m -> { + if (m == null) + throw new NotFoundException(); + })) + .call(m -> Panache.withTransaction(() -> + helloAssoRepository.persist( + new HelloAssoRegisterModel(cm, m, data.getId())))) + .chain(m -> updateRegister(req, cm, m, true))) + .onFailure().recoverWithItem(throwable -> { + fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(), + item.getUser().getFirstName(), optional.get())); + return null; + }) + .replaceWithVoid(); + } else { + fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(), + item.getUser().getFirstName())); + } + } + + return uni.call(__ -> fail.isEmpty() ? Uni.createFrom().nullItem() : + reactiveMailer.send( + Mail.withText(cm.getData4(), + "FFSAF - Compétition - Erreur HelloAsso", + String.format( + """ + Bonjour, + + Une erreur a été rencontrée lors de l'enregistrement d'une inscription à votre compétition %s pour les combattants suivants: + %s + + Cordialement, + L'intranet de la FFSAF + """, cm.getName(), String.join("\r\n", fail)) + ).setFrom("FFSAF ").setReplyTo("support@ffsaf.fr") + ).onFailure().invoke(e -> LOGGER.error("Fail to send email", e))); + }) + .onFailure().invoke(Throwable::printStackTrace) + .map(__ -> Response.ok().build()); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/WebhookService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/WebhookService.java index 9e011bc..d675609 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/WebhookService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/WebhookService.java @@ -13,13 +13,25 @@ public class WebhookService { @Inject CheckoutService checkoutService; + @Inject + CompetitionService competitionService; + @ConfigProperty(name = "helloasso.organizationSlug") String organizationSlug; public Uni helloAssoNotification(HelloassoNotification notification) { - if (notification.getEventType().equals("Payment")){ - if (notification.getData().getOrder().getOrganizationSlug().equalsIgnoreCase(organizationSlug)){ - return checkoutService.paymentStatusChange(notification.getData().getState(), notification.getMetadata()); + if (notification.getEventType().equals("Payment")) { + if (notification.getData().getOrder().getFormType().equals("Checkout")) { + if (notification.getData().getOrder().getOrganizationSlug().equalsIgnoreCase(organizationSlug)) { + return checkoutService.paymentStatusChange(notification.getData().getState(), + notification.getMetadata()); + } + } else if (notification.getData().getOrder().getFormType().equals("Event")) { + return competitionService.unregisterHelloAsso(notification.getData()); + } + }else if (notification.getEventType().equals("Order")){ + if (notification.getData().getFormType().equals("Event")) { + return competitionService.registerHelloAsso(notification.getData()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/client/dto/NotificationData.java b/src/main/java/fr/titionfire/ffsaf/rest/client/dto/NotificationData.java index 8d79e6e..b364b87 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/client/dto/NotificationData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/client/dto/NotificationData.java @@ -5,6 +5,8 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @Data @NoArgsConstructor @AllArgsConstructor @@ -12,11 +14,14 @@ import lombok.NoArgsConstructor; public class NotificationData { private Order order; private Integer id; + private String formSlug; + private String formType; private String organizationSlug; private String checkoutIntentId; private String oldSlugOrganization; // Pour les changements de nom d'association private String newSlugOrganization; private String state; // Pour les formulaires + private List items; @Data @@ -26,5 +31,35 @@ public class NotificationData { public static class Order { private Integer id; private String organizationSlug; + private String formSlug; + private String formType; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @RegisterForReflection + public static class Item { + private String name; + private User user; + private List customFields; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @RegisterForReflection + public static class User { + private String firstName; + private String lastName; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @RegisterForReflection + public static class CustomField { + private String name; + private String answer; } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java index eb9c50b..11fa9aa 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -1,9 +1,13 @@ package fr.titionfire.ffsaf.rest.data; import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor @RegisterForReflection public class RegisterRequestData { private Long licence;