package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.LicenceModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCombModel; import fr.titionfire.ffsaf.net2.request.SReqComb; import fr.titionfire.ffsaf.rest.data.MeData; import fr.titionfire.ffsaf.rest.data.SimpleLicence; import fr.titionfire.ffsaf.rest.data.SimpleMembre; import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.*; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheQuery; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import io.quarkus.scheduler.Scheduled; import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.hibernate.reactive.mutiny.Mutiny; import org.jboss.logging.Logger; import java.util.*; import java.util.concurrent.atomic.AtomicReference; @WithSession @ApplicationScoped public class MembreService { private static final Logger LOGGER = Logger.getLogger(MembreService.class); @Inject CombRepository repository; @Inject ClubRepository clubRepository; @Inject LicenceRepository licenceRepository; @Inject CompetitionRepository competitionRepository; @Inject ServerCustom serverCustom; @Inject KeycloakService keycloakService; @ConfigProperty(name = "upload_dir") String media; @Inject RegisterRepository registerRepository; @Inject LoggerService ls; public SimpleCombModel find(int licence, String np) throws Throwable { return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.find( "licence = ?1 AND (unaccent(lname) ILIKE unaccent(?2) OR unaccent(fname) ILIKE unaccent(?2))", licence, np).firstResult().map(SimpleCombModel::fromModel))); } public SimpleCombModel findByIdOptionalComb(long id) throws Throwable { return VertxContextSupport.subscribeAndAwait( () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel))); } final static String FIND_NAME_REQUEST = "unaccent(fname) ILIKE unaccent(?1) OR unaccent(lname) ILIKE unaccent(?1) " + "OR unaccent(fname || ' ' || lname) ILIKE unaccent(?1) OR unaccent(lname || ' ' || fname) ILIKE unaccent(?1)"; private Uni> getLicenceListe(int licenceRequest, int payState) { Uni> baseUni; String queryStr = "saison = ?1"; if (payState == 0) queryStr += " AND pay = FALSE"; if (payState == 1) queryStr += " AND pay = TRUE"; if (licenceRequest == 0 || licenceRequest == 1) baseUni = licenceRepository.list(queryStr, Utils.getSaison()); else if (licenceRequest == 2) baseUni = licenceRepository.list(queryStr + " AND validate = FALSE", Utils.getSaison()); else if (licenceRequest == 5) baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) >= 3", Utils.getSaison()); else if (licenceRequest == 6) baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) <= 2", Utils.getSaison()); else if (licenceRequest == 3) baseUni = licenceRepository.list(queryStr + " AND validate = TRUE", Utils.getSaison()); else baseUni = Uni.createFrom().item(new ArrayList<>()); return baseUni; } public Uni> searchAdmin(int limit, int page, String search, String club, int licenceRequest, int payState) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; String finalSearch = search; Uni> baseUni = getLicenceListe(licenceRequest, payState); return baseUni .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(ids -> { PanacheQuery query; String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN"); if (club == null || club.isBlank()) { query = repository.find( "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")", Sort.ascending("fname", "lname"), finalSearch, ids) .page(Page.ofSize(limit)); } else { if (club.equals("null")) { query = repository.find( "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")", Sort.ascending("fname", "lname"), finalSearch, ids).page(Page.ofSize(limit)); } else { query = repository.find( "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")", Sort.ascending("fname", "lname"), finalSearch, club + "%", ids) .page(Page.ofSize(limit)); } } return getPageResult(query, limit, page); }); } public Uni> search(int limit, int page, String search, int licenceRequest, int payState, String subject) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; String finalSearch = search; Uni> baseUni = getLicenceListe(licenceRequest, payState); return baseUni .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(ids -> { String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN"); return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { PanacheQuery query = repository.find( "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")", Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids) .page(Page.ofSize(limit)); return getPageResult(query, limit, page); }); }); } private Uni> getPageResult(PanacheQuery query, int limit, int page) { return Uni.createFrom().item(new PageResult()) .invoke(result -> result.setPage(page)) .invoke(result -> result.setPage_size(limit)) .call(result -> query.count().invoke(result::setResult_count)) .call(result -> query.pageCount() .invoke(Unchecked.consumer(pages -> { if (page > pages) throw new DBadRequestException("Page out of range"); })) .invoke(result::setPage_count)) .call(result -> query.page(Page.of(page, limit)).list() .map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList()) .invoke(result::setResult)); } public Uni> getAllExport(String subject) { return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> repository.list("club = ?1", membreModel.getClub())) .chain(membres -> licenceRepository.list("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres) .map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList())); } public Uni allImporte(String subject, List data) { if (data == null) return Uni.createFrom().nullItem(); final List data2 = data.stream() .filter(dataIn -> dataIn.getNom() != null && !dataIn.getNom() .isBlank() && dataIn.getPrenom() != null && !dataIn.getPrenom().isBlank()).toList(); if (data2.isEmpty()) return Uni.createFrom().nullItem(); AtomicReference clubModel = new AtomicReference<>(); LOGGER.debugf("Membre import (size=%d)", data2.size()); for (SimpleMembreInOutData simpleMembreInOutData : data2) { LOGGER.debugf("-> %s", simpleMembreInOutData.toString()); } return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { clubModel.set(membreModel.getClub()); if (data2.stream().noneMatch(d -> d.getLicence() != null)) return Uni.createFrom().item(new ArrayList()); return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3", data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(), data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(), data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank()) .toList()); }) .call(Unchecked.function(membres -> { for (MembreModel membreModel : membres) { if (!Objects.equals(membreModel.getClub(), clubModel.get())) { LOGGER.info("Similar membres found: " + membreModel); throw new DForbiddenException( "Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club"); } } Uni uniResult = Uni.createFrom().voidItem(); for (SimpleMembreInOutData dataIn : data2) { MembreModel model = membres.stream() .filter(m -> (dataIn.getLicence() != null && Objects.equals(m.getLicence(), dataIn.getLicence())) || m.getLname().equals(dataIn.getNom()) && m.getFname() .equals(dataIn.getPrenom()) || (dataIn.getEmail() != null && !dataIn.getEmail() .isBlank() && Objects.equals(m.getFname(), dataIn.getEmail()))).findFirst() .orElseGet(() -> { MembreModel mm = new MembreModel(); mm.setClub(clubModel.get()); mm.setLicences(new ArrayList<>()); mm.setCountry("FR"); return mm; }); if (model.getId() != null) { LOGGER.debugf("updating -> %s", dataIn.toString()); } else { LOGGER.debugf("creating -> %s", dataIn.toString()); } if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); } } boolean add = model.getId() == null; if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException( "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide."); } ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model); ls.logChange("Prénom", model.getFname(), dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model); model.setLname(dataIn.getNom().toUpperCase()); model.setFname(dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1)); if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) { ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model); model.setEmail(dataIn.getEmail()); } model.setGenre(Genre.fromString(dataIn.getGenre())); if (dataIn.getBirthdate() != null) { if (model.getBirth_date() == null || !Objects.equals(model.getBirth_date().getTime(), dataIn.getBirthdate().getTime())) ls.logChange("Date de naissance", model.getBirth_date(), dataIn.getBirthdate(), model); model.setBirth_date(dataIn.getBirthdate()); model.setCategorie(Utils.getCategoryFormBirthDate(model.getBirth_date(), new Date())); } uniResult = uniResult .call(() -> Panache.withTransaction(() -> repository.persist(model) .chain(membreModel1 -> dataIn.isLicenceCurrent() ? licenceRepository.find( "membre.id = ?1 AND saison = ?2", membreModel1.getId(), Utils.getSaison()) .firstResult() .call(l -> { if (l == null) { l = new LicenceModel(); l.setMembre(membreModel1); l.setClub_id(clubModel.get().getId()); l.setValidate(false); l.setSaison(Utils.getSaison()); } l.setCertificate(dataIn.getCertif()); return licenceRepository.persist(l); }) : licenceRepository.delete( "membre = ?1 AND saison = ?2 AND validate = false", membreModel1, Utils.getSaison())))); if (add) uniResult = uniResult.call(() -> ls.logAAdd(model)); else uniResult = uniResult.call(() -> ls.append()); } return uniResult; })) .map(__ -> "OK"); } public Uni getById(long id) { return repository.findById(id); } public Uni getByIdWithLicence(long id) { return repository.findById(id) .call(m -> Mutiny.fetch(m.getLicences())); } public Uni getByAccountId(String subject) { return repository.find("userId = ?1", subject).firstResult(); } public Uni getByLicence(long licence) { return repository.find("licence = ?1", licence).firstResult(); } public Uni update(long id, FullMemberForm membre) { return update(repository.findById(id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) throw new DBadRequestException("Email déjà utiliser"); }))) .chain(membreModel -> clubRepository.findById(membre.getClub()) .map(club -> new Pair<>(membreModel, club))) .onItem().transform(pair -> { MembreModel m = pair.getKey(); ls.logChange("Rôle", m.getRole(), membre.getRole(), m); m.setRole(membre.getRole()); ls.logChange("Club", m.getClub(), pair.getValue(), m); m.setClub(pair.getValue()); ls.logChange("Grade d'arbitrage", m.getGrade_arbitrage(), membre.getGrade_arbitrage(), m); m.setGrade_arbitrage(membre.getGrade_arbitrage()); return m; }), membre, true); } public Uni update(long id, FullMemberForm membre, SecurityCtx securityCtx) { return update(repository.findById(id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) throw new DBadRequestException("Email déjà utiliser"); }))) .invoke(Unchecked.consumer(membreModel -> { if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) throw new DForbiddenException(); if (StringSimilarity.similarity(membreModel.getLname().toUpperCase(), membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity( membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) { throw new DBadRequestException( "Pour enregistrer un nouveau membre, veuillez utilisez le bouton prévue a cette effet."); } })) .invoke(Unchecked.consumer(membreModel -> { RoleAsso source = RoleAsso.MEMBRE; if (securityCtx.roleHas("club_president")) source = RoleAsso.PRESIDENT; else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE; else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU; if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level) throw new DForbiddenException("Permission insuffisante"); })) .onItem().transform(target -> { if (!securityCtx.getSubject().equals(target.getUserId())) { ls.logChange("Rôle", target.getRole(), membre.getRole(), target); target.setRole(membre.getRole()); } return target; }), membre, false); } private Uni update(Uni uni, FullMemberForm membre, boolean admin) { return uni.chain(target -> { ls.logChange("Prénom", target.getFname(), membre.getFname(), target); target.setFname(membre.getFname()); ls.logChange("Nom", target.getLname(), membre.getLname(), target); target.setLname(membre.getLname().toUpperCase()); ls.logChange("Pays", target.getCountry(), membre.getCountry(), target); target.setCountry(membre.getCountry()); if (membre.getBirth_date() != null && (target.getBirth_date() == null || !Objects.equals( target.getBirth_date().getTime(), membre.getBirth_date().getTime()))) { ls.logChange("Date de naissance", target.getBirth_date(), membre.getBirth_date(), target); target.setBirth_date(membre.getBirth_date()); target.setCategorie(Utils.getCategoryFormBirthDate(membre.getBirth_date(), new Date())); } ls.logChange("Genre", target.getGenre(), membre.getGenre(), target); target.setGenre(membre.getGenre()); ls.logChange("Email", target.getEmail(), membre.getEmail(), target); target.setEmail(membre.getEmail()); return Panache.withTransaction(() -> repository.persist(target)).call(() -> ls.append()); }) .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) .call(membreModel -> (admin && membreModel.getUserId() != null) ? ((membreModel.getClub() != null) ? keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : keycloakService.clearUser(membreModel.getUserId())) : Uni.createFrom().nullItem()) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(), membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem()) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom() .nullItem()) .call(membreModel -> { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, -7); Date dateLimit = calendar.getTime(); return competitionRepository.list("date > ?1", dateLimit) .call(l -> l.isEmpty() ? Uni.createFrom().nullItem() : Uni.join().all(l.stream().map(competitionModel -> registerRepository.update( "categorie = ?1, club = ?2 where competition = ?3 AND membre = ?4", membreModel.getCategorie(), membreModel.getClub(), competitionModel, membreModel) ).toList()).andFailFast()); }) .call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3", (membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel, Utils.getSaison())) .call(membreModel -> membre.getPhoto_data().length > 0 ? ls.logAUpdate("Photo", membreModel) : Uni.createFrom().nullItem()) .map(__ -> "OK"); } public Uni add(FullMemberForm input) { return clubRepository.findById(input.getClub()) .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { if (c > 0) throw new DBadRequestException("Email déjà utiliser"); }))) .chain(clubModel -> { MembreModel model = getMembreModel(input, clubModel); return Panache.withTransaction(() -> repository.persist(model)); }) .call(membreModel -> ls.logAAdd(membreModel)) .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) .map(MembreModel::getId); } public Uni add(FullMemberForm input, String subject) { return repository.find("userId = ?1", subject).firstResult() .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { if (c > 0) throw new DBadRequestException("Email déjà utiliser"); }))) .call(membreModel -> repository.count( "unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND club = ?3", input.getLname(), input.getFname(), membreModel.getClub()) .invoke(Unchecked.consumer(c -> { if (c > 0) throw new DBadRequestException("Membre déjà existent"); }))) .chain(membreModel -> { MembreModel model = getMembreModel(input, membreModel.getClub()); model.setRole(RoleAsso.MEMBRE); model.setGrade_arbitrage(GradeArbitrage.NA); return Panache.withTransaction(() -> repository.persist(model)); }) .call(membreModel -> ls.logAAdd(membreModel)) .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) .map(MembreModel::getId); } public Uni delete(long id) { return repository.findById(id) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) .call(membreModel -> ls.logADelete(membreModel)) .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) .map(__ -> "Ok"); } public Uni delete(long id, SecurityCtx securityCtx) { return repository.findById(id) .invoke(Unchecked.consumer(membreModel -> { if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) throw new DForbiddenException(); })) .invoke(Unchecked.consumer(membreModel -> { if (membreModel.getLicence() != null) { throw new DBadRequestException( "Impossible de supprimer un membre qui a déjà un numéro de licence"); } })) .call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count() .invoke(Unchecked.consumer(l -> { if (l > 0) throw new DBadRequestException("Impossible de supprimer un membre avec des licences"); }))) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) .call(membreModel -> ls.logADelete(membreModel)) .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) .call(__ -> Utils.deleteMedia(id, media, "ppMembre")) .map(__ -> "Ok"); } public Uni setUserId(Long id, String id1) { return repository.findById(id).chain(membreModel -> { ls.logChange("KC UUID", membreModel.getUserId(), id1, membreModel); membreModel.setUserId(id1); return Panache.withTransaction(() -> repository.persist(membreModel)) .call(() -> ls.append()); }); } private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) { MembreModel model = new MembreModel(); model.setFname(input.getFname()); model.setLname(input.getLname()); model.setEmail(input.getEmail()); model.setLicence(null); model.setGenre(input.getGenre()); model.setCountry(input.getCountry()); model.setBirth_date(input.getBirth_date()); model.setCategorie(Utils.getCategoryFormBirthDate(input.getBirth_date(), new Date())); model.setClub(clubModel); model.setRole(input.getRole()); model.setGrade_arbitrage(input.getGrade_arbitrage()); return model; } public Uni> getSimilar(String fname, String lname) { return repository.listAll().map(membreModels -> membreModels.stream() .filter(m -> StringSimilarity.similarity(m.getFname(), fname) <= 3 && StringSimilarity.similarity(m.getLname(), lname) <= 3) .map(SimpleMembre::fromModel).toList()); } public Uni getMembre(String subject) { MeData meData = new MeData(); return repository.find("userId = ?1", subject).firstResult() .invoke(meData::setMembre) .chain(membreModel -> Mutiny.fetch(membreModel.getLicences())) .map(licences -> licences.stream().map(SimpleLicence::fromModel).toList()) .invoke(meData::setLicences) .map(__ -> meData); } @Scheduled(cron = "0 0 1 1 9 ?") Uni everySeason() { return repository.list("birth_date IS NOT NULL") .chain(l -> Uni.join().all(l.stream().map(m -> { m.setCategorie(Utils.getCategoryFormBirthDate(m.getBirth_date(), new Date())); return Panache.withTransaction(() -> repository.persist(m)); }).toList()).andCollectFailures()) .map(__ -> null); } }