Initial commit

This commit is contained in:
sidibe
2025-08-25 18:26:02 +00:00
commit 69b0fad8e8
201 changed files with 9146 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
package com.pmumali.simple.service;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Combinaison;
import com.pmumali.simple.model.Course;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CalculRapportService {
public Map<Combinaison, Double> calculerRapports(
List<Combinaison> combinaisonsPayables,
double masseAPartager) {
Map<Combinaison, Double> rapports = new HashMap<>();
if (combinaisonsPayables.size() == 1) {
// Cas arrivée normale
double rapport = masseAPartager / combinaisonsPayables.get(0).getNombreMises();
rapports.put(combinaisonsPayables.get(0), Math.max(1.1, rapport));
} else {
// Cas Dead-Heat
double beneficeParCombinaison = masseAPartager / combinaisonsPayables.size();
for (Combinaison combinaison : combinaisonsPayables) {
double rapport = beneficeParCombinaison / combinaison.getNombreMises();
rapports.put(combinaison, Math.max(1.1, rapport));
}
}
return rapports;
}
/**
* Vérifie si une combinaison est gagnante selon les positions d'arrivée
*/
public boolean isCombinaisonGagnante(Combinaison combinaison, List<Cheval> classement) {
if (classement.size() < 2) return false;
Cheval c1 = combinaison.getCheval1();
Cheval c2 = combinaison.getCheval2();
// Les deux chevaux doivent être dans les 2 premiers (ordre quelconque)
return classement.subList(0, 2).contains(c1) &&
classement.subList(0, 2).contains(c2);
}
/**
* Calcule le nombre de mises pour une combinaison donnée
*/
public long compterMisesCombinaison(Course course, Combinaison combinaison) {
return course.getParis().stream()
.filter(p -> p.getChevauxJumeles().containsAll(
List.of(combinaison.getCheval1(), combinaison.getCheval2())
))
.count();
}
public Map<Combinaison, Double> calculerRapportsJumelePlace(
List<Combinaison> combinaisonsPayables,
double masseAPartager) {
Map<Combinaison, Double> rapports = new HashMap<>();
if (combinaisonsPayables.isEmpty()) {
return rapports;
}
// Article 5a: Arrivée normale - division en 3 parts égales
if (!combinaisonsPayables.get(0).getCourse().isDeadHeat()) {
double part = masseAPartager / 3;
for (Combinaison combinaison : combinaisonsPayables) {
double rapport = part / compterMisesCombinaison(combinaison);
rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport));
}
return rapports;
}
// Article 5b: Dead-Heat
Course course = combinaisonsPayables.get(0).getCourse();
Map<Integer, List<Cheval>> parPosition = course.getChevaux().stream()
.filter(c -> !c.isEstNonPartant())
.collect(Collectors.groupingBy(Cheval::getPositionArrivee));
List<Cheval> premiers = parPosition.getOrDefault(1, Collections.emptyList());
List<Cheval> deuxiemes = parPosition.getOrDefault(2, Collections.emptyList());
List<Cheval> troisiemes = parPosition.getOrDefault(3, Collections.emptyList());
// Article 5b1: Dead-Heat 3+ premiers
if (premiers.size() >= 3) {
double part = masseAPartager / combinaisonsPayables.size();
for (Combinaison combinaison : combinaisonsPayables) {
double rapport = part / compterMisesCombinaison(combinaison);
rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport));
}
return rapports;
}
// Article 5b2: Dead-Heat 2 premiers + 1+ troisième
if (premiers.size() == 2 && !troisiemes.isEmpty()) {
// Répartition en 3 tiers
double tiers = masseAPartager / 3;
// 1er tiers: combinaison des 2 premiers
Combinaison combinaisonPremiers = new Combinaison(premiers.get(0), premiers.get(1));
double rapportPremiers = tiers / compterMisesCombinaison(combinaisonPremiers);
rapports.put(combinaisonPremiers, Math.max(RAPPORT_MINIMUM, rapportPremiers));
// 2ème tiers: premier1 avec troisièmes
double partParCombinaison = tiers / troisiemes.size();
for (Cheval troisieme : troisiemes) {
Combinaison combinaison = new Combinaison(premiers.get(0), troisieme);
double rapport = partParCombinaison / compterMisesCombinaison(combinaison);
rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport));
}
// 3ème tiers: premier2 avec troisièmes
for (Cheval troisieme : troisiemes) {
Combinaison combinaison = new Combinaison(premiers.get(1), troisieme);
double rapport = partParCombinaison / compterMisesCombinaison(combinaison);
rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport));
}
return rapports;
}
// ... autres cas Dead-Heat (implémenter de manière similaire)
return rapports;
}
}

View File

@@ -0,0 +1,48 @@
package com.pmumali.simple.service;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Combinaison;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class FormulaireService {
public double calculerCoutFormule(int nombreChevaux, boolean formuleComplete) {
// Article 7: Tableaux des combinaisons
int nbCombinaisons = formuleComplete ?
nombreChevaux * (nombreChevaux - 1) / 2 :
nombreChevaux;
return nbCombinaisons * 500; // 500 FCFA par combinaison
}
public List<Combinaison> genererCombinaisonsFormule(
List<Cheval> chevauxSelectionnes,
boolean formuleComplete) {
List<Combinaison> combinaisons = new ArrayList<>();
if (formuleComplete) {
// Toutes les combinaisons 2 à 2
for (int i = 0; i < chevauxSelectionnes.size(); i++) {
for (int j = i + 1; j < chevauxSelectionnes.size(); j++) {
combinaisons.add(new Combinaison(
chevauxSelectionnes.get(i),
chevauxSelectionnes.get(j)
));
}
}
} else {
// Formule simplifiée (champ total/partiel)
Cheval base = chevauxSelectionnes.get(0);
for (int i = 1; i < chevauxSelectionnes.size(); i++) {
combinaisons.add(new Combinaison(base, chevauxSelectionnes.get(i)));
}
}
return combinaisons;
}
}

View File

@@ -0,0 +1,309 @@
package com.pmumali.simple.service;
import com.pmumali.simple.dto.CombinaisonDto;
import com.pmumali.model.*;
import com.pmumali.simple.repository.ChevalRepository;
import com.pmumali.simple.repository.CourseRepository;
import com.pmumali.simple.repository.PariRepository;
import com.pmumali.simple.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class PariService {
@Autowired
private PariRepository pariRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
public Pari placerPariJumele(Long clientId, Long courseId,
List<Long> chevauxIds, double montant) {
// Vérification mise minimum
if (montant < 500) {
throw new IllegalArgumentException("La mise minimale est de 500 FCFA");
}
// Vérification limite de paris (20x mise min = 10 000 FCFA)
double totalParisClient = pariRepository
.sumByClientAndCourse(clientId, courseId);
if (totalParisClient + montant > 10000) {
throw new IllegalArgumentException("Limite de mise dépassée");
}
// Création du pari
Pari pari = new Pari();
// ... initialisation
return pariRepository.save(pari);
}
public ResultatCourse calculerResultats(Long courseId) throws Exception {
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new Exception("Course non trouvée"));
// Implémentation des règles de calcul
return calculerResultatsSelonReglement(course);
}
private ResultatCourse calculerResultatsSelonReglement(Course course) {
ResultatCourse resultat = new ResultatCourse();
// 1. Vérifier les non-partants (Article 4)
List<Cheval> nonPartants = course.getChevaux().stream()
.filter(Cheval::isEstNonPartant)
.collect(Collectors.toList());
// 2. Calcul selon Dead-Heat (Article 3)
if (course.isDeadHeat()) {
calculerDeadHeat(resultat, course);
} else {
calculerArriveeNormale(resultat, course);
}
// 3. Appliquer tirelire si nécessaire (Article 9)
if (resultat.getCombinaisonsPayables().isEmpty()) {
resultat.setTirelire(true);
}
return resultat;
}
/**
* Détermine les combinaisons payables selon les règles du PMU Mali
*/
private List<Combinaison> determinerCombinaisonsPayables(Course course) {
List<Cheval> chevauxArrives = course.getChevaux().stream()
.filter(c -> !c.isEstNonPartant())
.sorted(Comparator.comparingInt(Cheval::getPositionArrivee))
.collect(Collectors.toList());
// Article 4: Remboursement si non-partant
if (course.getChevaux().stream().anyMatch(Cheval::isEstNonPartant)) {
return Collections.emptyList();
}
// Article 3: Gestion Dead-Heat
if (course.isDeadHeat()) {
return calculerCombinaisonsDeadHeat(chevauxArrives);
}
// Article 5: Arrivée normale
return calculerCombinaisonsNormales(chevauxArrives);
}
/**
* Implémentation de l'article 3 - Cas Dead-Heat
*/
private List<Combinaison> calculerCombinaisonsDeadHeat(List<Cheval> chevauxArrives) {
List<Combinaison> combinaisons = new ArrayList<>();
// Groupes par position d'arrivée
Map<Integer, List<Cheval>> parPosition = chevauxArrives.stream()
.collect(Collectors.groupingBy(Cheval::getPositionArrivee));
List<Cheval> premiers = parPosition.getOrDefault(1, Collections.emptyList());
List<Cheval> deuxiemes = parPosition.getOrDefault(2, Collections.emptyList());
// Cas Dead-Heat premier place (Article 3a)
if (premiers.size() >= 2) {
for (int i = 0; i < premiers.size(); i++) {
for (int j = i + 1; j < premiers.size(); j++) {
combinaisons.add(new Combinaison(premiers.get(i), premiers.get(j)));
}
}
}
// Cas Dead-Heat deuxième place (Article 3b)
else if (deuxiemes.size() >= 2) {
Cheval premier = premiers.get(0);
for (Cheval deuxieme : deuxiemes) {
combinaisons.add(new Combinaison(premier, deuxieme));
}
}
return combinaisons;
}
/**
* Implémentation de l'article 5 - Arrivée normale
*/
private List<Combinaison> calculerCombinaisonsNormales(List<Cheval> chevauxArrives) {
if (chevauxArrives.size() < 2) {
return Collections.emptyList();
}
Cheval premier = chevauxArrives.get(0);
Cheval deuxieme = chevauxArrives.get(1);
return List.of(new Combinaison(premier, deuxieme));
}
/**
* Calcule la masse à partager selon l'article 5
*/
private double calculerMasseAPartager(Course course, List<Combinaison> combinaisonsPayables) {
double totalEnjeux = course.getParis().stream()
.mapToDouble(Pari::getMontantMise)
.sum();
double montantRembourse = course.getParis().stream()
.filter(p -> p.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant))
.mapToDouble(Pari::getMontantMise)
.sum();
// Article 5: MAP = RNET - MREMB - PRELEV
double prelevementsLegaux = totalEnjeux * 0.15; // Exemple: 15% de prélèvement
return totalEnjeux - montantRembourse - prelevementsLegaux;
}
/**
* Convertit les combinaisons en DTO pour la réponse API
*/
private List<CombinaisonDto> convertToDto(List<Combinaison> combinaisons) {
return combinaisons.stream()
.map(c -> new CombinaisonDto(
c.getCheval1().getNom(),
c.getCheval2().getNom(),
c.getNombreMises()
))
.collect(Collectors.toList());
}
/**
* Convertit les rapports en format lisible
*/
private Map<String, Double> convertRapports(Map<Combinaison, Double> rapports) {
return rapports.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey().getCheval1().getNom() + "-" + e.getKey().getCheval2().getNom(),
Map.Entry::getValue
));
}
private List<Combinaison> determinerCombinaisonsPayablesJumelePlace(Course course) {
List<Cheval> chevauxArrives = course.getChevaux().stream()
.filter(c -> !c.isEstNonPartant())
.sorted(Comparator.comparingInt(Cheval::getPositionArrivee))
.collect(Collectors.toList());
// Article 4: Remboursement si non-partant
if (course.getChevaux().stream().anyMatch(Cheval::isEstNonPartant)) {
return Collections.emptyList();
}
// Article 8: Moins de 3 chevaux arrivés
if (chevauxArrives.size() < 3) {
return Collections.emptyList();
}
// Article 3: Gestion Dead-Heat
if (course.isDeadHeat()) {
return calculerCombinaisonsDeadHeatJumelePlace(chevauxArrives);
}
// Cas normal - Article 1
return calculerCombinaisonsNormalesJumelePlace(chevauxArrives);
}
private List<Combinaison> calculerCombinaisonsNormalesJumelePlace(List<Cheval> chevauxArrives) {
List<Combinaison> combinaisons = new ArrayList<>();
Cheval premier = chevauxArrives.get(0);
Cheval deuxieme = chevauxArrives.get(1);
Cheval troisieme = chevauxArrives.get(2);
// Toutes les combinaisons 2 parmi 3
combinaisons.add(new Combinaison(premier, deuxieme));
combinaisons.add(new Combinaison(premier, troisieme));
combinaisons.add(new Combinaison(deuxieme, troisieme));
return combinaisons;
}
private List<Combinaison> calculerCombinaisonsDeadHeatJumelePlace(List<Cheval> chevauxArrives) {
List<Combinaison> combinaisons = new ArrayList<>();
Map<Integer, List<Cheval>> parPosition = chevauxArrives.stream()
.collect(Collectors.groupingBy(Cheval::getPositionArrivee));
List<Cheval> premiers = parPosition.getOrDefault(1, Collections.emptyList());
List<Cheval> deuxiemes = parPosition.getOrDefault(2, Collections.emptyList());
List<Cheval> troisiemes = parPosition.getOrDefault(3, Collections.emptyList());
// Article 3a: Dead-Heat à la première place (3+ chevaux)
if (premiers.size() >= 3) {
for (int i = 0; i < premiers.size(); i++) {
for (int j = i + 1; j < premiers.size(); j++) {
combinaisons.add(new Combinaison(premiers.get(i), premiers.get(j)));
}
}
return combinaisons;
}
// Article 3b: Dead-Heat 2 premiers + 1+ troisième
if (premiers.size() == 2 && !troisiemes.isEmpty()) {
// Combinaison des deux premiers
combinaisons.add(new Combinaison(premiers.get(0), premiers.get(1)));
// Combinaisons premier1 avec troisièmes
for (Cheval troisieme : troisiemes) {
combinaisons.add(new Combinaison(premiers.get(0), troisieme));
}
// Combinaisons premier2 avec troisièmes
for (Cheval troisieme : troisiemes) {
combinaisons.add(new Combinaison(premiers.get(1), troisieme));
}
return combinaisons;
}
// Article 3c: Dead-Heat à la deuxième place
if (!deuxiemes.isEmpty() && deuxiemes.size() >= 2) {
Cheval premier = premiers.get(0);
// Combinaisons premier avec deuxièmes
for (Cheval deuxieme : deuxiemes) {
combinaisons.add(new Combinaison(premier, deuxieme));
}
// Combinaisons deuxièmes entre eux
for (int i = 0; i < deuxiemes.size(); i++) {
for (int j = i + 1; j < deuxiemes.size(); j++) {
combinaisons.add(new Combinaison(deuxiemes.get(i), deuxiemes.get(j)));
}
}
return combinaisons;
}
// Article 3d: Dead-Heat à la troisième place
if (!troisiemes.isEmpty() && troisiemes.size() >= 2) {
Cheval premier = premiers.get(0);
Cheval deuxieme = deuxiemes.get(0);
// Combinaison premier-deuxième
combinaisons.add(new Combinaison(premier, deuxieme));
// Combinaisons premier avec troisièmes
for (Cheval troisieme : troisiemes) {
combinaisons.add(new Combinaison(premier, troisieme));
}
// Combinaisons deuxième avec troisièmes
for (Cheval troisieme : troisiemes) {
combinaisons.add(new Combinaison(deuxieme, troisieme));
}
return combinaisons;
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,43 @@
package com.pmumali.simple.service;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Combinaison;
import com.pmumali.simple.model.Course;
import java.util.List;
public class PariServiceTest {
@Test
public void testCombinaisonsPayables_Normal() {
Course course = new Course();
course.setDeadHeat(false);
Cheval c1 = new Cheval(); c1.setPositionArrivee(1);
Cheval c2 = new Cheval(); c2.setPositionArrivee(2);
Cheval c3 = new Cheval(); c3.setPositionArrivee(3);
course.setChevaux(List.of(c1, c2, c3));
List<Combinaison> result = pariService.determinerCombinaisonsPayablesJumelePlace(course);
assertEquals(3, result.size());
assertTrue(result.contains(new Combinaison(c1, c2)));
assertTrue(result.contains(new Combinaison(c1, c3)));
assertTrue(result.contains(new Combinaison(c2, c3)));
}
@Test
public void testDeadHeat_TroisPremiers() {
Course course = new Course();
course.setDeadHeat(true);
Cheval c1 = new Cheval(); c1.setPositionArrivee(1);
Cheval c2 = new Cheval(); c2.setPositionArrivee(1);
Cheval c3 = new Cheval(); c3.setPositionArrivee(1);
course.setChevaux(List.of(c1, c2, c3));
List<Combinaison> result = pariService.determinerCombinaisonsPayablesJumelePlace(course);
assertEquals(3, result.size()); // C(3,2) = 3 combinaisons
}
}

View File

@@ -0,0 +1,116 @@
package com.pmumali.simple.service;
import com.pmumali.simple.dto.PariRequest;
import com.pmumali.simple.exception.PariException;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Client;
import com.pmumali.simple.model.Course;
import com.pmumali.simple.model.Pari;
import com.pmumali.simple.model.enums.TypePari;
import com.pmumali.simple.repository.PariRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ValidationPariService {
@Autowired
private PariRepository pariRepository;
public void validerPari(Pari pari) {
// Article 2: Limitation des enjeux
validerLimiteEnjeux(pari);
// Article 4: Chevaux non-partants
validerChevauxPartants(pari.getCourse(), pari.getChevauxJumeles());
// Article 6: Formules combinées
validerFormule(pari);
}
private void validerLimiteEnjeux(Pari pari) {
double totalMises = pariRepository
.sumByClientAndCourse(pari.getClient().getId(), pari.getCourse().getId());
if (totalMises + pari.getMontantMise() > 10000) {
throw new PariException(PariException.ErrorCode.LIMITE_MISE_DEPASSEE, "Limite de mise dépassée (20x500 FCFA maximum)");
}
}
/**
* Valide la requête de pari selon les règles métier
*/
public void validerPariRequest(PariRequest request) {
if (request.getMontantMise() < 500) {
throw new PariException(PariException.ErrorCode.MISE_MINIMALE_NON_ATTEINTE,"La mise minimale est de 500 FCFA (Article 1)");
}
if (request.getChevauxIds() == null || request.getChevauxIds().size() != 2) {
throw new PariException(PariException.ErrorCode.PARI_INVALIDE,"Un pari Jumelé Gagnant doit porter sur exactement 2 chevaux");
}
}
/**
* Valide le pari selon toutes les règles du règlement
*/
public void validerPari(Client client, Course course, List<Cheval> chevaux, double montantMise) {
// Article 2: Limitation des enjeux
validerLimiteEnjeux(client, course, montantMise);
// Article 4: Chevaux non-partants
validerChevauxPartants(course, chevaux);
// Article 10: Course non annulée
if (course.isEstAnnulee()) {
throw new PariException(PariException.ErrorCode.COURSE_ANNULEE,"Course annulée - tous les paris seront remboursés (Article 8)");
}
// Vérifie que les chevaux appartiennent bien à la course
if (chevaux.stream().anyMatch(c -> !c.getCourse().equals(course))) {
throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT,"Un ou plusieurs chevaux ne font pas partie de cette course");
}
}
/**
* Implémentation de l'article 2 - Limitation des enjeux
*/
private void validerLimiteEnjeux(Client client, Course course, double nouvelleMise) {
double totalMises = pariRepository.sumByClientAndCourse(client.getId(), course.getId());
double limite = 20 * 500; // 20 fois la mise de base (500 FCFA)
if (totalMises + nouvelleMise > limite) {
throw new PariException( PariException.ErrorCode.LIMITE_MISE_DEPASSEE,
String.format("Limite de mise dépassée (max %,.0f FCFA par course selon Article 2)", limite)
);
}
}
/**
* Implémentation de l'article 4 - Chevaux non-partants
*/
private void validerChevauxPartants(Course course, List<Cheval> chevaux) {
if (chevaux.stream().anyMatch(Cheval::isEstNonPartant)) {
throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT,
"Pari non valide : un ou plusieurs chevaux sont non-partants (Article 4)"
);
}
}
public void validerPariJumelePlace(Pari pari) {
// Article 1: Vérification que c'est bien un Jumelé Placé
if (pari.getTypePari() != TypePari.JUMELEC_PLACE) {
throw new PariException(PariException.ErrorCode.FORMULE_INVALIDE ,"Ce n'est pas un pari Jumelé Placé");
}
// Article 2: Limitation des enjeux (identique)
validerLimiteEnjeux(pari.getClient(), pari.getCourse(), pari.getMontantMise());
// Article 4: Chevaux non-partants
if (pari.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant)) {
throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT,"Pari non valide: cheval non-partant (Article 4)");
}
}
}