feat: helloasso competition register

This commit is contained in:
Thibaut Valentin 2025-08-20 21:59:26 +02:00
parent 11dca5630c
commit 2a1bdfbdcb
6 changed files with 220 additions and 8 deletions

View File

@ -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;
}
}

View File

@ -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<HelloAssoRegisterModel, Long> {
}

View File

@ -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<RegisterModel> updateRegister(Long id, RegisterRequestData data, CompetitionModel c,
private Uni<RegisterModel> 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<Response> 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<Response> 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<String> place = List.of(cm.getData3().toLowerCase().split(";"));
List<String> 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<Long> 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 <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
})
.onFailure().invoke(Throwable::printStackTrace)
.map(__ -> Response.ok().build());
}
}

View File

@ -13,13 +13,25 @@ public class WebhookService {
@Inject
CheckoutService checkoutService;
@Inject
CompetitionService competitionService;
@ConfigProperty(name = "helloasso.organizationSlug")
String organizationSlug;
public Uni<Response> 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());
}
}

View File

@ -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<Item> 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<CustomField> 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;
}
}

View File

@ -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;