Fin intégration jumele gagnant

This commit is contained in:
sidibe
2025-08-28 08:28:44 +00:00
parent dc771d8cd8
commit d157d4d55a
70 changed files with 1924 additions and 2018 deletions

View File

@@ -0,0 +1,33 @@
package com.pmumali.ch1_simple.controller;
import com.pmumali.ch1_simple.model.Cheval;
import com.pmumali.ch1_simple.service.ChevalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/chevaux")
public class ChevalController {
@Autowired
private ChevalService chevalService;
@PostMapping
public ResponseEntity<Cheval> ajouterCheval(@RequestBody Cheval cheval) {
return ResponseEntity.ok(chevalService.ajouterCheval(cheval));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<List<Cheval>> obtenirChevauxParCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(chevalService.obtenirChevauxParCourse(courseId));
}
@GetMapping("/ecurie/{nomEcurie}")
public ResponseEntity<List<Cheval>> obtenirChevauxParEcurie(@PathVariable String nomEcurie) {
return ResponseEntity.ok(chevalService.obtenirChevauxParEcurie(nomEcurie));
}
}

View File

@@ -0,0 +1,43 @@
package com.pmumali.ch1_simple.controller;
import com.pmumali.ch1_simple.model.Course;
import com.pmumali.ch1_simple.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@Autowired
private CourseService courseService;
@PostMapping
public ResponseEntity<Course> creerCourse(@RequestBody Course course) {
return ResponseEntity.ok(courseService.creerCourse(course));
}
@GetMapping
public ResponseEntity<List<Course>> obtenirToutesCourses() {
return ResponseEntity.ok(courseService.obtenirToutesCourses());
}
@GetMapping("/{id}")
public ResponseEntity<Course> obtenirCourseParId(@PathVariable Long id) {
Course course = courseService.obtenirCourseParId(id);
return course != null ? ResponseEntity.ok(course) : ResponseEntity.notFound().build();
}
@GetMapping("/terminees")
public ResponseEntity<List<Course>> obtenirCoursesTerminees() {
return ResponseEntity.ok(courseService.obtenirCoursesTerminees());
}
@GetMapping("/avenir")
public ResponseEntity<List<Course>> obtenirCoursesAVenir() {
return ResponseEntity.ok(courseService.obtenirCoursesAVenir());
}
}

View File

@@ -0,0 +1,28 @@
package com.pmumali.ch1_simple.controller;
import com.pmumali.ch1_simple.model.Gains;
import com.pmumali.ch1_simple.model.ResultatCourse;
import com.pmumali.ch1_simple.service.GainsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/gains")
public class GainsController {
@Autowired
private GainsService gainsService;
@PostMapping("/calculer/{courseId}")
public ResponseEntity<Gains> calculerGains(@PathVariable Long courseId, @RequestBody ResultatCourse resultat) {
return ResponseEntity.ok(gainsService.calculerGains(courseId, resultat));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<Gains> obtenirGainsParCourse(@PathVariable Long courseId) {
Gains gains = gainsService.obtenirGainsParCourse(courseId);
return gains != null ? ResponseEntity.ok(gains) : ResponseEntity.notFound().build();
}
}

View File

@@ -0,0 +1,39 @@
package com.pmumali.ch1_simple.controller;
import com.pmumali.ch1_simple.model.PariSimple;
import com.pmumali.ch1_simple.service.PariSimpleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/paris")
public class PariController {
@Autowired
private PariSimpleService pariService;
@PostMapping
public ResponseEntity<PariSimple> placerPari(@RequestBody PariSimple pari) {
return ResponseEntity.ok(pariService.placerPari(pari));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<List<PariSimple>> obtenirParisParCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(pariService.obtenirParisParCourse(courseId));
}
@GetMapping("/course/{courseId}/type/{typePari}")
public ResponseEntity<List<PariSimple>> obtenirParisParCourseEtType(
@PathVariable Long courseId, @PathVariable String typePari) {
return ResponseEntity.ok(pariService.obtenirParisParCourseEtType(courseId, typePari));
}
@GetMapping("/cheval/{chevalId}")
public ResponseEntity<List<PariSimple>> obtenirParisParCheval(@PathVariable Long chevalId) {
return ResponseEntity.ok(pariService.obtenirParisParCheval(chevalId));
}
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.ch1_simple.dto;
import lombok.Data;
@Data
public class ChevalDto {
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
private String nomEcurie;
private Long courseId;
}

View File

@@ -0,0 +1,16 @@
package com.pmumali.ch1_simple.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class CourseDto {
private Long id;
private String nom;
private LocalDateTime date;
private String lieu;
private int nombreChevauxInscrits;
private boolean estTerminee;
private List<Long> chevauxIds;
}

View File

@@ -0,0 +1,17 @@
package com.pmumali.ch1_simple.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class GainsDto {
private Long id;
private Long courseId;
private double masseGagnant;
private double massePlace;
private double rapportGagnant;
private double rapportPlace;
private double montantCagnotte;
private LocalDateTime dateCalcul;
}

View File

@@ -0,0 +1,16 @@
package com.pmumali.ch1_simple.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PariSimpleDto {
private Long id;
private String typePari;
private double mise;
private LocalDateTime datePari;
private Long chevalId;
private Long courseId;
private boolean estPaye;
}

View File

@@ -0,0 +1,15 @@
package com.pmumali.ch1_simple.dto;
import lombok.Data;
import java.util.List;
@Data
public class ResultatCourseDto {
private Long id;
private Long courseId;
private List<Long> chevauxPremiers;
private List<Long> chevauxDeuxiemes;
private List<Long> chevauxTroisiemes;
private boolean aDeadHeat;
}

View File

@@ -0,0 +1,25 @@
package com.pmumali.ch1_simple.model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "cheval")
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
private String nomEcurie;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
}

View File

@@ -0,0 +1,32 @@
package com.pmumali.ch1_simple.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private LocalDateTime date;
private String lieu;
private int nombreChevauxInscrits;
private boolean estTerminee;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<Cheval> chevaux;
@OneToOne(mappedBy = "course", cascade = CascadeType.ALL)
private ResultatCourse resultat;
}

View File

@@ -0,0 +1,31 @@
package com.pmumali.ch1_simple.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "gains")
public class Gains {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private double masseGagnant;
private double massePlace;
private double rapportGagnant;
private double rapportPlace;
private double montantCagnotte;
private LocalDateTime dateCalcul;
}

View File

@@ -0,0 +1,31 @@
package com.pmumali.ch1_simple.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "pari_simple")
public class PariSimple {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String typePari; // GAGNANT ou PLACE
private double mise = 500.0;
private LocalDateTime datePari;
@ManyToOne
@JoinColumn(name = "cheval_id")
private Cheval cheval;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private boolean estPaye;
}

View File

@@ -0,0 +1,32 @@
package com.pmumali.ch1_simple.model;
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "resultat_course")
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "course_id")
private Course course;
@ElementCollection
private List<Long> chevauxPremiers; // Pour gérer les dead-heats
@ElementCollection
private List<Long> chevauxDeuxiemes; // Pour gérer les dead-heats
@ElementCollection
private List<Long> chevauxTroisiemes; // Pour gérer les dead-heats
private boolean aDeadHeat;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.ch1_simple.repository;
import com.pmumali.ch1_simple.model.Cheval;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
List<Cheval> findByCourseId(Long courseId);
List<Cheval> findByNomEcurie(String nomEcurie);
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.ch1_simple.repository;
import com.pmumali.ch1_simple.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByEstTerminee(boolean estTerminee);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.ch1_simple.repository;
import com.pmumali.ch1_simple.model.Gains;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GainsRepository extends JpaRepository<Gains, Long> {
Gains findByCourseId(Long courseId);
}

View File

@@ -0,0 +1,15 @@
package com.pmumali.ch1_simple.repository;
import com.pmumali.ch1_simple.model.PariSimple;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PariSimpleRepository extends JpaRepository<PariSimple, Long> {
List<PariSimple> findByCourseId(Long courseId);
List<PariSimple> findByCourseIdAndTypePari(Long courseId, String typePari);
List<PariSimple> findByChevalId(Long chevalId);
List<PariSimple> findByChevalIdAndTypePari(Long chevalId, String typePari);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.ch1_simple.repository;
import com.pmumali.ch1_simple.model.ResultatCourse;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
ResultatCourse findByCourseId(Long courseId);
}

View File

@@ -0,0 +1,27 @@
package com.pmumali.ch1_simple.service;
import com.pmumali.ch1_simple.model.Cheval;
import com.pmumali.ch1_simple.repository.ChevalRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ChevalService {
@Autowired
private ChevalRepository chevalRepository;
public Cheval ajouterCheval(Cheval cheval) {
return chevalRepository.save(cheval);
}
public List<Cheval> obtenirChevauxParCourse(Long courseId) {
return chevalRepository.findByCourseId(courseId);
}
public List<Cheval> obtenirChevauxParEcurie(String nomEcurie) {
return chevalRepository.findByNomEcurie(nomEcurie);
}
}

View File

@@ -0,0 +1,37 @@
package com.pmumali.ch1_simple.service;
import com.pmumali.ch1_simple.model.Course;
import com.pmumali.ch1_simple.repository.CourseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class CourseService {
@Autowired
private CourseRepository courseRepository;
public Course creerCourse(Course course) {
return courseRepository.save(course);
}
public List<Course> obtenirToutesCourses() {
return courseRepository.findAll();
}
public Course obtenirCourseParId(Long id) {
return courseRepository.findById(id).orElse(null);
}
public List<Course> obtenirCoursesTerminees() {
return courseRepository.findByEstTerminee(true);
}
public List<Course> obtenirCoursesAVenir() {
return courseRepository.findByEstTerminee(false);
}
}

View File

@@ -0,0 +1,479 @@
package com.pmumali.ch1_simple.service;
import com.pmumali.ch1_simple.model.*;
import com.pmumali.ch1_simple.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class GainsService {
@Autowired
private PariSimpleRepository pariRepository;
@Autowired
private GainsRepository gainsRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
@Autowired
private ResultatCourseRepository resultatRepository;
private static final double RAPPORT_MINIMUM = 1.1;
private static final double PRELEVEMENTS = 0.15; // 15% de prélèvements
public Gains calculerGains(Long courseId, ResultatCourse resultat) {
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
List<PariSimple> tousParis = pariRepository.findByCourseId(courseId);
// Calcul de la recette nette (Article 5)
double totalMises = tousParis.stream().mapToDouble(PariSimple::getMise).sum();
double recetteNette = totalMises;
// Calcul des remboursements (Article 4)
double montantRemboursements = calculerRemboursements(tousParis);
// Masse à partager (Article 5)
double masseAPartager = recetteNette - montantRemboursements - (recetteNette * PRELEVEMENTS);
// Séparation des paris GAGNANT et PLACE
List<PariSimple> parisGagnant = tousParis.stream()
.filter(p -> "GAGNANT".equals(p.getTypePari()))
.collect(Collectors.toList());
List<PariSimple> parisPlace = tousParis.stream()
.filter(p -> "PLACE".equals(p.getTypePari()))
.collect(Collectors.toList());
// Calcul des masses à partager pour chaque type de pari
double masseGagnant = masseAPartager * (parisGagnant.stream().mapToDouble(PariSimple::getMise).sum() / totalMises);
double massePlace = masseAPartager * (parisPlace.stream().mapToDouble(PariSimple::getMise).sum() / totalMises);
// Calcul des rapports
Gains gains = new Gains();
gains.setCourse(course);
gains.setMasseGagnant(masseGagnant);
gains.setMassePlace(massePlace);
gains.setDateCalcul(LocalDateTime.now());
// Calcul détaillé des gains selon les règles
calculerDetailsGains(gains, resultat, parisGagnant, parisPlace);
// Gérer la cagnotte si nécessaire (Article 6 et 7)
gererCagnotte(gains, resultat, parisGagnant, parisPlace);
return gainsRepository.save(gains);
}
private double calculerRemboursements(List<PariSimple> paris) {
double remboursements = 0.0;
for (PariSimple pari : paris) {
if (pari.getCheval().isEstNonPartant()) {
remboursements += pari.getMise();
pari.setEstPaye(true); // Marquer comme remboursé
}
}
return remboursements;
}
private void calculerDetailsGains(Gains gains, ResultatCourse resultat,
List<PariSimple> parisGagnant, List<PariSimple> parisPlace) {
// Calcul des rapports GAGNANT
if (resultat.isADeadHeat()) {
calculerRapportsGagnantDeadHeat(gains, resultat, parisGagnant);
} else {
calculerRapportsGagnantNormal(gains, resultat, parisGagnant);
}
// Calcul des rapports PLACE
if (resultat.isADeadHeat()) {
calculerRapportsPlaceDeadHeat(gains, resultat, parisPlace);
} else {
calculerRapportsPlaceNormal(gains, resultat, parisPlace);
}
// Garantir un rapport minimum de 1.1 par unité de mise (Article 5d)
gains.setRapportGagnant(Math.max(gains.getRapportGagnant(), RAPPORT_MINIMUM));
gains.setRapportPlace(Math.max(gains.getRapportPlace(), RAPPORT_MINIMUM));
}
private void calculerRapportsGagnantNormal(Gains gains, ResultatCourse resultat, List<PariSimple> parisGagnant) {
// Article 5a: Cas d'arrivée normale - Calcul du rapport "gagnant"
if (resultat.getChevauxPremiers().isEmpty()) {
gains.setRapportGagnant(0.0);
return;
}
// Gestion des écuries (Article 2)
Long chevalGagnantId = resultat.getChevauxPremiers().get(0);
Cheval chevalGagnant = chevalRepository.findById(chevalGagnantId).orElse(null);
if (chevalGagnant == null) {
gains.setRapportGagnant(0.0);
return;
}
// Trouver tous les chevaux de la même écurie
List<Cheval> chevauxEcurie = Collections.singletonList(chevalGagnant);
if (chevalGagnant.getNomEcurie() != null) {
chevauxEcurie = chevalRepository.findByNomEcurie(chevalGagnant.getNomEcurie());
}
// Calculer le total des mises sur les chevaux de l'écurie
double totalMisesEcurie = 0.0;
for (Cheval cheval : chevauxEcurie) {
double misesCheval = parisGagnant.stream()
.filter(p -> p.getCheval().getId().equals(cheval.getId()))
.mapToDouble(PariSimple::getMise)
.sum();
totalMisesEcurie += misesCheval;
}
if (totalMisesEcurie > 0) {
gains.setRapportGagnant(gains.getMasseGagnant() / totalMisesEcurie);
} else {
gains.setRapportGagnant(0.0);
}
}
private void calculerRapportsGagnantDeadHeat(Gains gains, ResultatCourse resultat, List<PariSimple> parisGagnant) {
// Article 5a: Cas d'arrivée "dead-heat" - Calcul des rapports "gagnant"
if (resultat.getChevauxPremiers().isEmpty()) {
gains.setRapportGagnant(0.0);
return;
}
// Calculer le bénéfice à répartir (Article 5a)
double totalMisesPayables = 0.0;
for (Long chevalId : resultat.getChevauxPremiers()) {
double misesCheval = parisGagnant.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
totalMisesPayables += misesCheval;
}
double beneficeARepartir = gains.getMasseGagnant() - totalMisesPayables;
if (beneficeARepartir <= 0) {
gains.setRapportGagnant(0.0);
return;
}
// Diviser le bénéfice en autant de parties qu'il y a de chevaux classés premiers
double partParCheval = beneficeARepartir / resultat.getChevauxPremiers().size();
// Pour chaque cheval, calculer son rapport
double rapportTotal = 0.0;
int chevauxAvecMises = 0;
for (Long chevalId : resultat.getChevauxPremiers()) {
double misesCheval = parisGagnant.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
if (misesCheval > 0) {
double rapportCheval = (partParCheval / misesCheval) + 1;
rapportTotal += rapportCheval;
chevauxAvecMises++;
}
}
// Article 6: Si aucun mise sur un cheval, partager entre les autres
if (chevauxAvecMises > 0) {
gains.setRapportGagnant(rapportTotal / chevauxAvecMises);
} else {
gains.setRapportGagnant(0.0);
}
}
private void calculerRapportsPlaceNormal(Gains gains, ResultatCourse resultat, List<PariSimple> parisPlace) {
// Article 5b: Cas d'arrivée normale - Calcul du rapport "placé"
int nombreChevauxInscrits = gains.getCourse().getNombreChevauxInscrits();
List<Long> chevauxPayables = new ArrayList<>();
// Déterminer les chevaux payables selon le nombre de partants
if (nombreChevauxInscrits >= 4 && nombreChevauxInscrits <= 7) {
// 2 premiers
chevauxPayables.addAll(resultat.getChevauxPremiers());
chevauxPayables.addAll(resultat.getChevauxDeuxiemes());
} else if (nombreChevauxInscrits >= 8) {
// 3 premiers
chevauxPayables.addAll(resultat.getChevauxPremiers());
chevauxPayables.addAll(resultat.getChevauxDeuxiemes());
chevauxPayables.addAll(resultat.getChevauxTroisiemes());
}
if (chevauxPayables.isEmpty()) {
gains.setRapportPlace(0.0);
return;
}
// Calculer le bénéfice à répartir (Article 5b)
double totalMisesPayables = 0.0;
for (Long chevalId : chevauxPayables) {
double misesCheval = parisPlace.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
totalMisesPayables += misesCheval;
}
double beneficeARepartir = gains.getMassePlace() - totalMisesPayables;
if (beneficeARepartir <= 0) {
gains.setRapportPlace(0.0);
return;
}
// Diviser le bénéfice en autant de parties qu'il y a de chevaux payables
double partParCheval = beneficeARepartir / chevauxPayables.size();
// Pour chaque cheval, calculer son rapport
double rapportTotal = 0.0;
int chevauxAvecMises = 0;
for (Long chevalId : chevauxPayables) {
double misesCheval = parisPlace.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
if (misesCheval > 0) {
double rapportCheval = (partParCheval / misesCheval) + 1;
rapportTotal += rapportCheval;
chevauxAvecMises++;
}
}
// Article 6: Si aucun mise sur un cheval, partager entre les autres
if (chevauxAvecMises > 0) {
gains.setRapportPlace(rapportTotal / chevauxAvecMises);
} else {
gains.setRapportPlace(0.0);
}
}
private void calculerRapportsPlaceDeadHeat(Gains gains, ResultatCourse resultat, List<PariSimple> parisPlace) {
// Article 5b et 5c: Cas d'arrivée "dead-heat" - Calcul des rapports "placé"
int nombreChevauxInscrits = gains.getCourse().getNombreChevauxInscrits();
// Déterminer les chevaux payables selon le nombre de partants et le type de dead-heat
List<Long> chevauxPayables = determinerChevauxPayablesPlace(resultat, nombreChevauxInscrits);
if (chevauxPayables.isEmpty()) {
gains.setRapportPlace(0.0);
return;
}
// Calculer le bénéfice à répartir
double totalMisesPayables = 0.0;
for (Long chevalId : chevauxPayables) {
double misesCheval = parisPlace.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
totalMisesPayables += misesCheval;
}
double beneficeARepartir = gains.getMassePlace() - totalMisesPayables;
if (beneficeARepartir <= 0) {
gains.setRapportPlace(0.0);
return;
}
// Application des règles spécifiques pour les dead-heats (Article 5c)
if (nombreChevauxInscrits < 8) {
beneficeARepartir = appliquerReglesPlaceMoinsHuit(resultat, beneficeARepartir);
} else {
beneficeARepartir = appliquerReglesPlaceHuitPlus(resultat, beneficeARepartir);
}
// Diviser le bénéfice entre les chevaux payables
double partParCheval = beneficeARepartir / chevauxPayables.size();
// Pour chaque cheval, calculer son rapport
double rapportTotal = 0.0;
int chevauxAvecMises = 0;
for (Long chevalId : chevauxPayables) {
double misesCheval = parisPlace.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
if (misesCheval > 0) {
double rapportCheval = (partParCheval / misesCheval) + 1;
rapportTotal += rapportCheval;
chevauxAvecMises++;
}
}
// Article 6: Si aucun mise sur un cheval, partager entre les autres
if (chevauxAvecMises > 0) {
gains.setRapportPlace(rapportTotal / chevauxAvecMises);
} else {
gains.setRapportPlace(0.0);
}
}
private List<Long> determinerChevauxPayablesPlace(ResultatCourse resultat, int nombreChevauxInscrits) {
List<Long> chevauxPayables = new ArrayList<>();
// Article 3: Déterminer les chevaux payables selon le nombre de partants
if (nombreChevauxInscrits >= 4 && nombreChevauxInscrits <= 7) {
// 2 premiers
chevauxPayables.addAll(resultat.getChevauxPremiers());
chevauxPayables.addAll(resultat.getChevauxDeuxiemes());
} else if (nombreChevauxInscrits >= 8) {
// 3 premiers
chevauxPayables.addAll(resultat.getChevauxPremiers());
chevauxPayables.addAll(resultat.getChevauxDeuxiemes());
chevauxPayables.addAll(resultat.getChevauxTroisiemes());
}
return chevauxPayables;
}
private double appliquerReglesPlaceMoinsHuit(ResultatCourse resultat, double beneficeARepartir) {
// Article 5c: Règles pour les courses de moins de 8 chevaux avec dead-heat
int nbPremiers = resultat.getChevauxPremiers().size();
int nbDeuxiemes = resultat.getChevauxDeuxiemes().size();
if (nbPremiers > 1) {
// Plus d'un cheval classé premier
return beneficeARepartir / nbPremiers;
} else if (nbDeuxiemes > 1) {
// Plus d'un cheval classé deuxième
double partPremier = beneficeARepartir / 2;
double partDeuxiemes = beneficeARepartir / 2;
return partPremier + (partDeuxiemes / nbDeuxiemes);
}
return beneficeARepartir;
}
private double appliquerReglesPlaceHuitPlus(ResultatCourse resultat, double beneficeARepartir) {
// Article 5c: Règles pour les courses de 8 chevaux et plus avec dead-heat
int nbPremiers = resultat.getChevauxPremiers().size();
int nbDeuxiemes = resultat.getChevauxDeuxiemes().size();
int nbTroisiemes = resultat.getChevauxTroisiemes().size();
if (nbPremiers == 1 && nbDeuxiemes == 1) {
// Un seul premier et un seul deuxième, plusieurs troisièmes
double partPremier = beneficeARepartir / 3;
double partDeuxieme = beneficeARepartir / 3;
double partTroisiemes = beneficeARepartir / 3;
return partPremier + partDeuxieme + (partTroisiemes / nbTroisiemes);
} else if (nbPremiers == 1 && nbDeuxiemes > 1) {
// Un seul premier, plusieurs deuxièmes
double partPremier = beneficeARepartir / 3;
double partDeuxiemes = beneficeARepartir * 2 / 3;
return partPremier + (partDeuxiemes / nbDeuxiemes);
} else if (nbPremiers == 2) {
// Deux chevaux classés premiers
double partParPremier = beneficeARepartir / 3;
double partTroisiemes = beneficeARepartir / 3;
return (2 * partParPremier) + (partTroisiemes / nbTroisiemes);
} else if (nbPremiers > 2) {
// Plus de deux chevaux classés premiers
return beneficeARepartir / nbPremiers;
}
return beneficeARepartir;
}
private void gererCagnotte(Gains gains, ResultatCourse resultat,
List<PariSimple> parisGagnant, List<PariSimple> parisPlace) {
// Article 6 et 7: Gestion de la cagnotte (tirelire)
double montantCagnotte = 0.0;
// Vérifier les conditions pour la cagnotte GAGNANT
boolean cagnotteGagnant = false;
if (resultat.getChevauxPremiers().isEmpty()) {
// Aucun cheval classé premier
cagnotteGagnant = true;
} else {
// Vérifier s'il n'y a aucune mise sur les chevaux premiers
boolean aucuneMiseGagnant = true;
for (Long chevalId : resultat.getChevauxPremiers()) {
double mises = parisGagnant.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
if (mises > 0) {
aucuneMiseGagnant = false;
break;
}
}
cagnotteGagnant = aucuneMiseGagnant;
}
if (cagnotteGagnant) {
montantCagnotte += gains.getMasseGagnant();
gains.setMasseGagnant(0.0);
gains.setRapportGagnant(0.0);
}
// Vérifier les conditions pour la cagnotte PLACE
boolean cagnottePlace = false;
int nombreChevauxInscrits = gains.getCourse().getNombreChevauxInscrits();
// Article 6.3: Si moins de 4 chevaux ont pris part à la course
if (nombreChevauxInscrits < 4) {
cagnottePlace = true;
} else {
List<Long> chevauxPayables = determinerChevauxPayablesPlace(resultat, nombreChevauxInscrits);
if (chevauxPayables.isEmpty()) {
cagnottePlace = true;
} else {
// Vérifier s'il n'y a aucune mise sur les chevaux payables
boolean aucuneMisePlace = true;
for (Long chevalId : chevauxPayables) {
double mises = parisPlace.stream()
.filter(p -> p.getCheval().getId().equals(chevalId))
.mapToDouble(PariSimple::getMise)
.sum();
if (mises > 0) {
aucuneMisePlace = false;
break;
}
}
cagnottePlace = aucuneMisePlace;
}
}
if (cagnottePlace) {
montantCagnotte += gains.getMassePlace();
gains.setMassePlace(0.0);
gains.setRapportPlace(0.0);
}
gains.setMontantCagnotte(montantCagnotte);
// TODO: Implémenter la logique de report de la cagnotte selon l'article 7
}
public Gains obtenirGainsParCourse(Long courseId) {
return gainsRepository.findByCourseId(courseId);
}
}

View File

@@ -0,0 +1,52 @@
package com.pmumali.ch1_simple.service;
import com.pmumali.ch1_simple.model.Course;
import com.pmumali.ch1_simple.model.PariSimple;
import com.pmumali.ch1_simple.repository.ChevalRepository;
import com.pmumali.ch1_simple.repository.CourseRepository;
import com.pmumali.ch1_simple.repository.PariSimpleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PariSimpleService {
@Autowired
private PariSimpleRepository pariRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
private static final double MISE_DE_BASE = 500.0;
public PariSimple placerPari(PariSimple pari) {
// Vérifier que le type de pari est valide pour la course
Course course = pari.getCourse();
if ("GAGNANT".equals(pari.getTypePari()) && course.getNombreChevauxInscrits() < 2) {
throw new IllegalArgumentException("Pari GAGNANT impossible: moins de 2 chevaux");
}
if ("PLACE".equals(pari.getTypePari()) && course.getNombreChevauxInscrits() < 3) {
throw new IllegalArgumentException("Pari PLACE impossible: moins de 3 chevaux");
}
return pariRepository.save(pari);
}
public List<PariSimple> obtenirParisParCourse(Long courseId) {
return pariRepository.findByCourseId(courseId);
}
public List<PariSimple> obtenirParisParCourseEtType(Long courseId, String typePari) {
return pariRepository.findByCourseIdAndTypePari(courseId, typePari);
}
public List<PariSimple> obtenirParisParCheval(Long chevalId) {
return pariRepository.findByChevalId(chevalId);
}
}

View File

@@ -0,0 +1,32 @@
package com.pmumali.ch2_jumelegagnant.controller;
import com.pmumali.ch2_jumelegagnant.model.Cheval;
import com.pmumali.ch2_jumelegagnant.service.ChevalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/chevaux")
public class ChevalController {
@Autowired
private ChevalService chevalService;
@PostMapping
public ResponseEntity<Cheval> ajouterCheval(@RequestBody Cheval cheval) {
return ResponseEntity.ok(chevalService.ajouterCheval(cheval));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<List<Cheval>> obtenirChevauxParCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(chevalService.obtenirChevauxParCourse(courseId));
}
@GetMapping("/non-partants")
public ResponseEntity<List<Cheval>> obtenirChevauxNonPartants() {
return ResponseEntity.ok(chevalService.obtenirChevauxNonPartants());
}
}

View File

@@ -0,0 +1,48 @@
package com.pmumali.ch2_jumelegagnant.controller;
import com.pmumali.ch2_jumelegagnant.model.Course;
import com.pmumali.ch2_jumelegagnant.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@Autowired
private CourseService courseService;
@PostMapping
public ResponseEntity<Course> creerCourse(@RequestBody Course course) {
return ResponseEntity.ok(courseService.creerCourse(course));
}
@GetMapping
public ResponseEntity<List<Course>> obtenirToutesCourses() {
return ResponseEntity.ok(courseService.obtenirToutesCourses());
}
@GetMapping("/{id}")
public ResponseEntity<Course> obtenirCourseParId(@PathVariable Long id) {
Course course = courseService.obtenirCourseParId(id);
return course != null ? ResponseEntity.ok(course) : ResponseEntity.notFound().build();
}
@GetMapping("/terminees")
public ResponseEntity<List<Course>> obtenirCoursesTerminees() {
return ResponseEntity.ok(courseService.obtenirCoursesTerminees());
}
@GetMapping("/avenir")
public ResponseEntity<List<Course>> obtenirCoursesAVenir() {
return ResponseEntity.ok(courseService.obtenirCoursesAVenir());
}
@GetMapping("/annulees")
public ResponseEntity<List<Course>> obtenirCoursesAnnulees() {
return ResponseEntity.ok(courseService.obtenirCoursesAnnulees());
}
}

View File

@@ -0,0 +1,27 @@
package com.pmumali.ch2_jumelegagnant.controller;
import com.pmumali.ch2_jumelegagnant.model.Gains;
import com.pmumali.ch2_jumelegagnant.model.ResultatCourse;
import com.pmumali.ch2_jumelegagnant.service.GainsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/gains")
public class GainsController {
@Autowired
private GainsService gainsService;
@PostMapping("/calculer/{courseId}")
public ResponseEntity<Gains> calculerGains(@PathVariable Long courseId, @RequestBody ResultatCourse resultat) {
return ResponseEntity.ok(gainsService.calculerGains(courseId, resultat));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<Gains> obtenirGainsParCourse(@PathVariable Long courseId) {
Gains gains = gainsService.obtenirGainsParCourse(courseId);
return gains != null ? ResponseEntity.ok(gains) : ResponseEntity.notFound().build();
}
}

View File

@@ -0,0 +1,46 @@
package com.pmumali.ch2_jumelegagnant.controller;
import com.pmumali.ch2_jumelegagnant.model.PariJumeleGagnant;
import com.pmumali.ch2_jumelegagnant.service.PariJumeleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/paris-jumele")
public class PariJumeleController {
@Autowired
private PariJumeleService pariService;
@PostMapping
public ResponseEntity<PariJumeleGagnant> placerPari(@RequestBody PariJumeleGagnant pari) {
return ResponseEntity.ok(pariService.placerPari(pari));
}
@GetMapping("/course/{courseId}")
public ResponseEntity<List<PariJumeleGagnant>> obtenirParisParCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(pariService.obtenirParisParCourse(courseId));
}
@GetMapping("/cheval/{chevalId}")
public ResponseEntity<List<PariJumeleGagnant>> obtenirParisParCheval(@PathVariable Long chevalId) {
return ResponseEntity.ok(pariService.obtenirParisParCheval(chevalId));
}
@GetMapping("/calculer-formule-combinee/{nombreChevaux}")
public ResponseEntity<Double> calculerCoutFormuleCombinee(@PathVariable int nombreChevaux) {
return ResponseEntity.ok(pariService.calculerCoutFormuleCombinee(nombreChevaux));
}
@GetMapping("/calculer-formule-champ")
public ResponseEntity<Double> calculerCoutFormuleChamp(
@RequestParam int nombreChevauxPartants,
@RequestParam int nombreChevauxSelectionnes,
@RequestParam boolean estChampTotal) {
return ResponseEntity.ok(pariService.calculerCoutFormuleChamp(
nombreChevauxPartants, nombreChevauxSelectionnes, estChampTotal));
}
}

View File

@@ -0,0 +1,12 @@
package com.pmumali.ch2_jumelegagnant.dto;
import lombok.Data;
@Data
public class ChevalDto {
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
private Long courseId;
}

View File

@@ -0,0 +1,17 @@
package com.pmumali.ch2_jumelegagnant.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class CourseDto {
private Long id;
private String nom;
private LocalDateTime date;
private String lieu;
private int nombreChevauxInscrits;
private boolean estTerminee;
private boolean estAnnulee;
private List<Long> chevauxIds;
}

View File

@@ -0,0 +1,16 @@
package com.pmumali.ch2_jumelegagnant.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class GainsDto {
private Long id;
private Long courseId;
private double masseAPartager;
private double montantCagnotte;
private List<Double> rapports;
private LocalDateTime dateCalcul;
}

View File

@@ -0,0 +1,18 @@
package com.pmumali.ch2_jumelegagnant.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PariJumeleDto {
private Long id;
private String typePari;
private double mise;
private LocalDateTime datePari;
private Long cheval1Id;
private Long cheval2Id;
private Long courseId;
private boolean estPaye;
private boolean estRembourse;
}

View File

@@ -0,0 +1,14 @@
package com.pmumali.ch2_jumelegagnant.dto;
import lombok.Data;
import java.util.List;
@Data
public class ResultatCourseDto {
private Long id;
private Long courseId;
private List<Long> chevauxPremiers;
private List<Long> chevauxDeuxiemes;
private boolean aDeadHeat;
}

View File

@@ -0,0 +1,23 @@
package com.pmumali.ch2_jumelegagnant.model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "cheval")
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
}

View File

@@ -0,0 +1,33 @@
package com.pmumali.ch2_jumelegagnant.model;
import jakarta.persistence.*;
import lombok.*;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private LocalDateTime date;
private String lieu;
private int nombreChevauxInscrits;
private boolean estTerminee;
private boolean estAnnulee;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<Cheval> chevaux;
@OneToOne(mappedBy = "course", cascade = CascadeType.ALL)
private ResultatCourse resultat;
}

View File

@@ -0,0 +1,30 @@
package com.pmumali.ch2_jumelegagnant.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "gains")
public class Gains {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private double masseAPartager;
private double montantCagnotte;
@ElementCollection
private List<Double> rapports; // Rapports pour chaque combinaison payable
private LocalDateTime dateCalcul;
}

View File

@@ -0,0 +1,36 @@
package com.pmumali.ch2_jumelegagnant.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "pari_jumele_gagnant")
public class PariJumeleGagnant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String typePari; // UNITAIRE, COMBINE, CHAMP_TOTAL, CHAMP_PARTIEL
private double mise = 500.0;
private LocalDateTime datePari;
@ManyToOne
@JoinColumn(name = "cheval1_id")
private Cheval cheval1;
@ManyToOne
@JoinColumn(name = "cheval2_id")
private Cheval cheval2;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private boolean estPaye;
private boolean estRembourse;
}

View File

@@ -0,0 +1,29 @@
package com.pmumali.ch2_jumelegagnant.model;
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "resultat_course")
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "course_id")
private Course course;
@ElementCollection
private List<Long> chevauxPremiers; // Pour gérer les dead-heats
@ElementCollection
private List<Long> chevauxDeuxiemes; // Pour gérer les dead-heats
private boolean aDeadHeat;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.ch2_jumelegagnant.repository;
import com.pmumali.ch2_jumelegagnant.model.Cheval;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
List<Cheval> findByCourseId(Long courseId);
List<Cheval> findByEstNonPartant(boolean estNonPartant);
}

View File

@@ -0,0 +1,12 @@
package com.pmumali.ch2_jumelegagnant.repository;
import com.pmumali.ch2_jumelegagnant.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByEstTerminee(boolean estTerminee);
List<Course> findByEstAnnulee(boolean estAnnulee);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.ch2_jumelegagnant.repository;
import com.pmumali.ch2_jumelegagnant.model.Gains;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GainsRepository extends JpaRepository<Gains, Long> {
Gains findByCourseId(Long courseId);
}

View File

@@ -0,0 +1,14 @@
package com.pmumali.ch2_jumelegagnant.repository;
import com.pmumali.ch2_jumelegagnant.model.PariJumeleGagnant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PariJumeleRepository extends JpaRepository<PariJumeleGagnant, Long> {
List<PariJumeleGagnant> findByCourseId(Long courseId);
List<PariJumeleGagnant> findByCheval1IdOrCheval2Id(Long cheval1Id, Long cheval2Id);
List<PariJumeleGagnant> findByEstRembourse(boolean estRembourse);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.ch2_jumelegagnant.repository;
import com.pmumali.ch2_jumelegagnant.model.ResultatCourse;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
ResultatCourse findByCourseId(Long courseId);
}

View File

@@ -0,0 +1,27 @@
package com.pmumali.ch2_jumelegagnant.service;
import com.pmumali.ch2_jumelegagnant.model.Cheval;
import com.pmumali.ch2_jumelegagnant.repository.ChevalRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ChevalService {
@Autowired
private ChevalRepository chevalRepository;
public Cheval ajouterCheval(Cheval cheval) {
return chevalRepository.save(cheval);
}
public List<Cheval> obtenirChevauxParCourse(Long courseId) {
return chevalRepository.findByCourseId(courseId);
}
public List<Cheval> obtenirChevauxNonPartants() {
return chevalRepository.findByEstNonPartant(true);
}
}

View File

@@ -0,0 +1,40 @@
package com.pmumali.ch2_jumelegagnant.service;
import com.pmumali.ch2_jumelegagnant.model.Course;
import com.pmumali.ch2_jumelegagnant.repository.CourseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CourseService {
@Autowired
private CourseRepository courseRepository;
public Course creerCourse(Course course) {
return courseRepository.save(course);
}
public List<Course> obtenirToutesCourses() {
return courseRepository.findAll();
}
public Course obtenirCourseParId(Long id) {
return courseRepository.findById(id).orElse(null);
}
public List<Course> obtenirCoursesTerminees() {
return courseRepository.findByEstTerminee(true);
}
public List<Course> obtenirCoursesAVenir() {
return courseRepository.findByEstTerminee(false);
}
public List<Course> obtenirCoursesAnnulees() {
return courseRepository.findByEstAnnulee(true);
}
}

View File

@@ -0,0 +1,329 @@
package com.pmumali.ch2_jumelegagnant.service;
import com.pmumali.ch2_jumelegagnant.model.Course;
import com.pmumali.ch2_jumelegagnant.model.Gains;
import com.pmumali.ch2_jumelegagnant.model.PariJumeleGagnant;
import com.pmumali.ch2_jumelegagnant.model.ResultatCourse;
import com.pmumali.ch2_jumelegagnant.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
@Service
@Transactional
public class GainsService {
@Autowired
private PariJumeleRepository pariRepository;
@Autowired
private GainsRepository gainsRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
@Autowired
private ResultatCourseRepository resultatRepository;
private static final double RAPPORT_MINIMUM = 1.1;
private static final double PRELEVEMENTS = 0.15; // 15% de prélèvements
public Gains calculerGains(Long courseId, ResultatCourse resultat) {
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
List<PariJumeleGagnant> tousParis = pariRepository.findByCourseId(courseId);
// Calcul de la recette nette (Article 5)
double totalMises = tousParis.stream().mapToDouble(PariJumeleGagnant::getMise).sum();
double recetteNette = totalMises;
// Calcul des remboursements (Article 4)
double montantRemboursements = calculerRemboursements(tousParis);
// Masse à partager (Article 5)
double masseAPartager = recetteNette - montantRemboursements - (recetteNette * PRELEVEMENTS);
// Déterminer les combinaisons payables
List<CombinaisonPayable> combinaisonsPayables = determinerCombinaisonsPayables(resultat);
// Calcul des rapports
Gains gains = new Gains();
gains.setCourse(course);
gains.setMasseAPartager(masseAPartager);
gains.setDateCalcul(LocalDateTime.now());
// Calcul détaillé des gains selon les règles
if (resultat.isADeadHeat()) {
calculerRapportsDeadHeat(gains, combinaisonsPayables, tousParis);
} else {
calculerRapportsNormaux(gains, combinaisonsPayables, tousParis);
}
// Gérer la cagnotte si nécessaire (Article 8 et 9)
gererCagnotte(gains, combinaisonsPayables);
return gainsRepository.save(gains);
}
private double calculerRemboursements(List<PariJumeleGagnant> paris) {
double remboursements = 0.0;
for (PariJumeleGagnant pari : paris) {
// Article 4: Remboursement si un ou deux chevaux non partants
if (pari.getCheval1().isEstNonPartant() || pari.getCheval2().isEstNonPartant()) {
remboursements += pari.getMise();
pari.setEstRembourse(true);
pariRepository.save(pari);
}
}
return remboursements;
}
private List<CombinaisonPayable> determinerCombinaisonsPayables(ResultatCourse resultat) {
List<CombinaisonPayable> combinaisons = new ArrayList<>();
// Article 1: Le pari est payable si les deux chevaux occupent les deux premières places
// Article 3: Gestion des dead-heats
if (resultat.isADeadHeat()) {
// Cas de dead-heat
if (resultat.getChevauxPremiers().size() >= 2) {
// Article 3a: Dead-heat à la première place
// Toutes les combinaisons des chevaux classés premiers pris deux à deux
for (int i = 0; i < resultat.getChevauxPremiers().size(); i++) {
for (int j = i + 1; j < resultat.getChevauxPremiers().size(); j++) {
combinaisons.add(new CombinaisonPayable(
resultat.getChevauxPremiers().get(i),
resultat.getChevauxPremiers().get(j)
));
}
}
} else if (resultat.getChevauxDeuxiemes().size() >= 2) {
// Article 3b: Dead-heat à la deuxième place
// Combinaisons du premier avec chaque deuxième
for (Long chevalPremier : resultat.getChevauxPremiers()) {
for (Long chevalDeuxieme : resultat.getChevauxDeuxiemes()) {
combinaisons.add(new CombinaisonPayable(chevalPremier, chevalDeuxieme));
}
}
}
} else {
// Cas normal
if (!resultat.getChevauxPremiers().isEmpty() && !resultat.getChevauxDeuxiemes().isEmpty()) {
combinaisons.add(new CombinaisonPayable(
resultat.getChevauxPremiers().get(0),
resultat.getChevauxDeuxiemes().get(0)
));
}
}
return combinaisons;
}
private void calculerRapportsNormaux(Gains gains, List<CombinaisonPayable> combinaisonsPayables,
List<PariJumeleGagnant> tousParis) {
// Article 5a: Cas d'arrivée normale
if (combinaisonsPayables.isEmpty()) {
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
return;
}
// Normalement, une seule combinaison payable en cas d'arrivée normale
CombinaisonPayable combinaisonPayable = combinaisonsPayables.get(0);
// Calculer le total des mises sur cette combinaison
double totalMises = calculerMisesSurCombinaison(combinaisonPayable, tousParis);
if (totalMises > 0) {
double rapport = gains.getMasseAPartager() / totalMises;
gains.setRapports(Collections.singletonList(Math.max(rapport, RAPPORT_MINIMUM)));
} else {
// Article 8a: Aucune mise sur la combinaison payable
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
}
}
private void calculerRapportsDeadHeat(Gains gains, List<CombinaisonPayable> combinaisonsPayables,
List<PariJumeleGagnant> tousParis) {
// Article 5b: Cas d'arrivée "dead heat"
if (combinaisonsPayables.isEmpty()) {
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
return;
}
// Calculer le total des mises sur toutes les combinaisons payables
double totalMisesPayables = 0.0;
Map<CombinaisonPayable, Double> misesParCombinaison = new HashMap<>();
for (CombinaisonPayable combinaison : combinaisonsPayables) {
double mises = calculerMisesSurCombinaison(combinaison, tousParis);
misesParCombinaison.put(combinaison, mises);
totalMisesPayables += mises;
}
// Calculer le bénéfice à répartir
double beneficeARepartir = gains.getMasseAPartager() - totalMisesPayables;
if (beneficeARepartir <= 0) {
// Article 8b: Gestion des cas où il n'y a pas de bénéfice
gererCasParticuliersDeadHeat(gains, combinaisonsPayables, misesParCombinaison);
return;
}
// Diviser le bénéfice en autant de parties qu'il y a de combinaisons payables
double partParCombinaison = beneficeARepartir / combinaisonsPayables.size();
// Calculer les rapports pour chaque combinaison
List<Double> rapports = new ArrayList<>();
for (CombinaisonPayable combinaison : combinaisonsPayables) {
double mises = misesParCombinaison.get(combinaison);
if (mises > 0) {
double rapport = (partParCombinaison / mises) + 1;
rapports.add(Math.max(rapport, RAPPORT_MINIMUM));
} else {
// Article 8b1: Aucune mise sur une combinaison payable
// Répartir le bénéfice entre les autres combinaisons
double partSupplementaire = partParCombinaison / (combinaisonsPayables.size() - 1);
// Nous ajouterons cette part aux autres combinaisons plus tard
rapports.add(0.0);
}
}
// Ajuster les rapports pour les combinaisons sans mises
ajusterRapportsSansMises(rapports, combinaisonsPayables, misesParCombinaison, partParCombinaison);
gains.setRapports(rapports);
}
private void gererCasParticuliersDeadHeat(Gains gains, List<CombinaisonPayable> combinaisonsPayables,
Map<CombinaisonPayable, Double> misesParCombinaison) {
// Article 8b: Gestion des cas particuliers de dead-heat
boolean aucuneMise = true;
for (Double mises : misesParCombinaison.values()) {
if (mises > 0) {
aucuneMise = false;
break;
}
}
if (aucuneMise) {
// Article 8b2, 8b3, 8b4: Aucune mise sur aucune combinaison payable
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
} else {
// Répartir la masse entre les combinaisons avec des mises
double totalMises = misesParCombinaison.values().stream().mapToDouble(Double::doubleValue).sum();
List<Double> rapports = new ArrayList<>();
for (CombinaisonPayable combinaison : combinaisonsPayables) {
double mises = misesParCombinaison.get(combinaison);
if (mises > 0) {
double rapport = gains.getMasseAPartager() / totalMises;
rapports.add(Math.max(rapport, RAPPORT_MINIMUM));
} else {
rapports.add(0.0);
}
}
gains.setRapports(rapports);
}
}
private void ajusterRapportsSansMises(List<Double> rapports, List<CombinaisonPayable> combinaisonsPayables,
Map<CombinaisonPayable, Double> misesParCombinaison, double partParCombinaison) {
// Compter le nombre de combinaisons sans mises
int combinaisonsSansMises = 0;
for (Double mises : misesParCombinaison.values()) {
if (mises == 0) {
combinaisonsSansMises++;
}
}
if (combinaisonsSansMises > 0) {
double partSupplementaire = partParCombinaison / (combinaisonsPayables.size() - combinaisonsSansMises);
for (int i = 0; i < rapports.size(); i++) {
if (rapports.get(i) > 0) {
rapports.set(i, rapports.get(i) + partSupplementaire / misesParCombinaison.get(combinaisonsPayables.get(i)));
}
}
}
}
private double calculerMisesSurCombinaison(CombinaisonPayable combinaison, List<PariJumeleGagnant> tousParis) {
double totalMises = 0.0;
for (PariJumeleGagnant pari : tousParis) {
if ((pari.getCheval1().getId().equals(combinaison.getCheval1Id()) &&
pari.getCheval2().getId().equals(combinaison.getCheval2Id())) ||
(pari.getCheval1().getId().equals(combinaison.getCheval2Id()) &&
pari.getCheval2().getId().equals(combinaison.getCheval1Id()))) {
totalMises += pari.getMise();
}
}
return totalMises;
}
private void gererCagnotte(Gains gains, List<CombinaisonPayable> combinaisonsPayables) {
// Article 8 et 9: Gestion de la cagnotte
if (combinaisonsPayables.isEmpty()) {
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
}
// Article 8c: Course annulée ou reportée
if (gains.getCourse().isEstAnnulee()) {
gains.setMontantCagnotte(gains.getMasseAPartager());
gains.setMasseAPartager(0.0);
}
}
// Classe interne pour représenter une combinaison payable
private static class CombinaisonPayable {
private Long cheval1Id;
private Long cheval2Id;
public CombinaisonPayable(Long cheval1Id, Long cheval2Id) {
this.cheval1Id = cheval1Id;
this.cheval2Id = cheval2Id;
}
public Long getCheval1Id() { return cheval1Id; }
public Long getCheval2Id() { return cheval2Id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CombinaisonPayable that = (CombinaisonPayable) o;
return (Objects.equals(cheval1Id, that.cheval1Id) &&
Objects.equals(cheval2Id, that.cheval2Id)) ||
(Objects.equals(cheval1Id, that.cheval2Id) &&
Objects.equals(cheval2Id, that.cheval1Id));
}
@Override
public int hashCode() {
// Hash code qui ne dépend pas de l'ordre des chevaux
long id1 = Math.min(cheval1Id, cheval2Id);
long id2 = Math.max(cheval1Id, cheval2Id);
return Objects.hash(id1, id2);
}
}
public Gains obtenirGainsParCourse(Long courseId) {
return gainsRepository.findByCourseId(courseId);
}
}

View File

@@ -0,0 +1,63 @@
package com.pmumali.ch2_jumelegagnant.service;
import com.pmumali.ch2_jumelegagnant.model.PariJumeleGagnant;
import com.pmumali.ch2_jumelegagnant.repository.ChevalRepository;
import com.pmumali.ch2_jumelegagnant.repository.CourseRepository;
import com.pmumali.ch2_jumelegagnant.repository.PariJumeleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PariJumeleService {
@Autowired
private PariJumeleRepository pariRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
private static final double MISE_DE_BASE = 500.0;
private static final double MISE_MAXIMALE = 20 * MISE_DE_BASE;
public PariJumeleGagnant placerPari(PariJumeleGagnant pari) {
// Vérification de la limite d'enjeu (Article 2)
if (pari.getMise() > MISE_MAXIMALE) {
pari.setMise(MISE_MAXIMALE);
// TODO: Implémenter le remboursement de la différence
}
return pariRepository.save(pari);
}
public List<PariJumeleGagnant> obtenirParisParCourse(Long courseId) {
return pariRepository.findByCourseId(courseId);
}
public List<PariJumeleGagnant> obtenirParisParCheval(Long chevalId) {
return pariRepository.findByCheval1IdOrCheval2Id(chevalId, chevalId);
}
// Méthode pour calculer le coût d'une formule combinée (Article 6a)
public double calculerCoutFormuleCombinee(int nombreChevauxSelectionnes) {
if (nombreChevauxSelectionnes < 2) {
return 0;
}
int nombreCombinaisons = (nombreChevauxSelectionnes * (nombreChevauxSelectionnes - 1)) / 2;
return nombreCombinaisons * MISE_DE_BASE;
}
// Méthode pour calculer le coût d'une formule champ (Article 6b)
public double calculerCoutFormuleChamp(int nombreChevauxPartants, int nombreChevauxSelectionnes, boolean estChampTotal) {
if (estChampTotal) {
return (nombreChevauxPartants - 1) * MISE_DE_BASE;
} else {
return nombreChevauxSelectionnes * MISE_DE_BASE;
}
}
}

View File

@@ -1,28 +0,0 @@
package com.pmumali.simple.controller;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Course;
import java.util.stream.Collectors;
public class CourseController {
private CourseDetailsDto convertToDetailsDto(Course course) {
CourseDetailsDto dto = new CourseDetailsDto();
dto.setId(course.getId());
dto.setNom(course.getNom());
dto.setDateCourse(course.getDateCourse());
dto.setChevaux(course.getChevaux().stream()
.map(this::convertChevalDto)
.collect(Collectors.toList()));
return dto;
}
private ChevalDto convertChevalDto(Cheval cheval) {
ChevalDto dto = new ChevalDto();
dto.setId(cheval.getId());
dto.setNom(cheval.getNom());
dto.setNonPartant(cheval.isEstNonPartant());
return dto;
}
}

View File

@@ -1,68 +0,0 @@
package com.pmumali.simple.controller;
import com.pmu.jumele.dto.PariRequest;
import com.pmu.mali.model.ResultatCourse;
import com.pmumali.simple.dto.PariResponse;
import com.pmumali.simple.dto.ResultatCourseDto;
import com.pmumali.simple.model.Pari;
import com.pmumali.simple.service.PariService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/paris")
public class PariController {
@Autowired
private PariService pariService;
@PostMapping("/jumelé")
public ResponseEntity<Pari> placerPari(
@RequestBody PariRequest pariRequest) {
Pari pari = pariService.placerPariJumele(
pariRequest.getClientId(),
pariRequest.getCourseId(),
pariRequest.getChevauxIds(),
pariRequest.getMontantMise()
);
return ResponseEntity.ok(pari);
}
@GetMapping("/resultats/{courseId}")
public ResponseEntity<ResultatCourse> calculerResultats(
@PathVariable Long courseId) {
return ResponseEntity.ok(pariService.calculerResultats(courseId));
}
@PostMapping("/jumele-place")
public ResponseEntity<PariResponse> placerPariJumelePlace(
@Valid @RequestBody PariRequest pariRequest) {
Pari pari = pariService.placerPariJumelePlace(pariRequest);
return ResponseEntity.ok(convertToResponse(pari));
}
@GetMapping("/resultats/jumele-place/{courseId}")
public ResponseEntity<ResultatCourseDto> getResultatsJumelePlace(
@PathVariable Long courseId) {
Pari
ResultatCourseDto resultat = pariService.calculerResultatsJumelePlace(courseId);
return ResponseEntity.ok(resultat);
}
private PariResponse convertToResponse(Pari pari) {
PariResponse response = new PariResponse();
response.setId(pari.getId());
response.setMontantMise(pari.getMontantMise());
response.setDatePari(pari.getDatePari());
response.setChevaux(pari.getChevauxJumeles().stream()
.map(Cheval::getNom)
.collect(Collectors.toList()));
response.setCourseNom(pari.getCourse().getNom());
return response;
}
}

View File

@@ -1,23 +0,0 @@
package com.pmumali.simple.dto;
import com.pmumali.simple.model.Cheval;
import lombok.Data;
@Data
public class ChevalDto {
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
// Constructeurs, Getters, Setters
public static ChevalDto fromEntity(Cheval cheval) {
ChevalDto dto = new ChevalDto();
dto.setId(cheval.getId());
dto.setNom(cheval.getNom());
dto.setNumero(cheval.getNumero());
dto.setEstNonPartant(cheval.isEstNonPartant());
return dto;
}
}

View File

@@ -1,12 +0,0 @@
package com.pmumali.simple.dto;
import lombok.Data;
@Data
public class CombinaisonDto {
private String cheval1;
private String cheval2;
private int nombreMises;
// Getters, setters
}

View File

@@ -1,15 +0,0 @@
package com.pmumali.simple.dto;
import lombok.Data;
import java.util.List;
@Data
public class PariRequest {
private Long clientId;
private Long courseId;
private List<Long> chevauxIds;
private double montantMise;
// Getters, setters
}

View File

@@ -1,211 +0,0 @@
package com.pmumali.simple.dto;
import com.pmumali.simple.model.Cheval;
import com.pmumali.simple.model.Pari;
import com.pmumali.simple.model.enums.TypePari;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
/**
* DTO pour la réponse API des paris
* Contient toutes les informations à exposer au client
*/
public class PariResponse {
private Long id;
private String numeroPari;
private TypePari typePari;
private double montantMise;
@JsonFormat(pattern = "dd/MM/yyyy HH:mm:ss")
private LocalDateTime datePari;
private String statut; // EN_COURS, GAGNANT, PERDANT, REMBOURSE
private Double gainsPotentiels;
private boolean estPaye;
// Informations course
private Long courseId;
private String courseNom;
@JsonFormat(pattern = "dd/MM/yyyy HH:mm")
private LocalDateTime dateCourse;
// Chevaux sélectionnés
private List<ChevalDto> chevaux;
// Informations client (simplifiées)
private String clientNomComplet;
private String clientNumero;
// Constructeurs
public PariResponse() {}
public PariResponse(Long id, String numeroPari, TypePari typePari, double montantMise,
LocalDateTime datePari, String statut, Double gainsPotentiels,
boolean estPaye, Long courseId, String courseNom,
LocalDateTime dateCourse) {
this.id = id;
this.numeroPari = numeroPari;
this.typePari = typePari;
this.montantMise = montantMise;
this.datePari = datePari;
this.statut = statut;
this.gainsPotentiels = gainsPotentiels;
this.estPaye = estPaye;
this.courseId = courseId;
this.courseNom = courseNom;
this.dateCourse = dateCourse;
}
// Getters et Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNumeroPari() {
return numeroPari;
}
public void setNumeroPari(String numeroPari) {
this.numeroPari = numeroPari;
}
public TypePari getTypePari() {
return typePari;
}
public void setTypePari(TypePari typePari) {
this.typePari = typePari;
}
public double getMontantMise() {
return montantMise;
}
public void setMontantMise(double montantMise) {
this.montantMise = montantMise;
}
public LocalDateTime getDatePari() {
return datePari;
}
public void setDatePari(LocalDateTime datePari) {
this.datePari = datePari;
}
public String getStatut() {
return statut;
}
public void setStatut(String statut) {
this.statut = statut;
}
public Double getGainsPotentiels() {
return gainsPotentiels;
}
public void setGainsPotentiels(Double gainsPotentiels) {
this.gainsPotentiels = gainsPotentiels;
}
public boolean isEstPaye() {
return estPaye;
}
public void setEstPaye(boolean estPaye) {
this.estPaye = estPaye;
}
public Long getCourseId() {
return courseId;
}
public void setCourseId(Long courseId) {
this.courseId = courseId;
}
public String getCourseNom() {
return courseNom;
}
public void setCourseNom(String courseNom) {
this.courseNom = courseNom;
}
public LocalDateTime getDateCourse() {
return dateCourse;
}
public void setDateCourse(LocalDateTime dateCourse) {
this.dateCourse = dateCourse;
}
public List<ChevalDto> getChevaux() {
return chevaux;
}
public void setChevaux(List<ChevalDto> chevaux) {
this.chevaux = chevaux;
}
public String getClientNomComplet() {
return clientNomComplet;
}
public void setClientNomComplet(String clientNomComplet) {
this.clientNomComplet = clientNomComplet;
}
public String getClientNumero() {
return clientNumero;
}
public void setClientNumero(String clientNumero) {
this.clientNumero = clientNumero;
}
// Méthode utilitaire de conversion depuis l'entité
public static PariResponse fromEntity(Pari pari) {
PariResponse response = new PariResponse(
pari.getId(),
"PMU-" + pari.getId(),
pari.getTypePari(),
pari.getMontantMise(),
pari.getDatePari(),
determinerStatut(pari),
pari.getGains(),
pari.isEstPaye(),
pari.getCourse().getId(),
pari.getCourse().getNom(),
pari.getCourse().getDateCourse()
);
response.setChevaux(pari.getChevauxJumeles().stream()
.map(ChevalDto::fromEntity)
.collect(Collectors.toList()));
response.setClientNomComplet(pari.getClient().getPrenom() + " " + pari.getClient().getNom());
response.setClientNumero(pari.getClient().getNumeroClient());
return response;
}
private static String determinerStatut(Pari pari) {
if (pari.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant)) {
return "REMBOURSE";
}
if (pari.isEstPaye()) {
return "GAGNANT";
}
if (pari.getCourse().isTerminee() && !pari.isEstPaye()) {
return "PERDANT";
}
return "EN_COURS";
}
}

View File

@@ -1,16 +0,0 @@
package com.pmumali.simple.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class ResultatCourseDto {
private Long courseId;
private List<CombinaisonDto> combinaisonsPayables;
private Map<String, Double> rapports;
private boolean tirelire;
// Getters, setters
}

View File

@@ -1,84 +0,0 @@
package com.pmumali.simple.exception;
/**
* Exception métier pour les erreurs spécifiques aux paris PMU Mali
* conforme aux règles du "Jumelé Placé"
*/
public class PariException extends RuntimeException {
private final ErrorCode errorCode;
private final String details;
// Codes d'erreur standardisés
public enum ErrorCode {
// Article 1 - Règles de base
MISE_MINIMALE_NON_ATTEINTE("La mise minimale est de 500 FCFA"),
// Article 2 - Limitation des enjeux
LIMITE_MISE_DEPASSEE("Limite de mise dépassée (20x500 FCFA maximum)"),
// Article 3 - Dead Heat
CALCUL_RAPPORT_IMPOSSIBLE("Impossible de calculer les rapports pour ce Dead Heat"),
// Article 4 - Non-partants
CHEVAL_NON_PARTANT("Pari invalide : cheval non-partant"),
// Article 5 - Calcul des rapports
RAPPORT_INVALIDE("Rapport calculé invalide (<1.1)"),
// Article 6 - Formules
FORMULE_INVALIDE("Formule de pari invalide"),
// Article 8 - Cas particuliers
COURSE_ANNULEE("Course annulée - paris remboursés"),
// Validations générales
PARI_INVALIDE("Pari invalide"),
SOLDE_INSUFFISANT("Solde insuffisant pour placer ce pari"),
CLIENT_BLOQUE("Compte client bloqué");
private final String message;
ErrorCode(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// Constructeurs
public PariException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.details = null;
}
public PariException(ErrorCode errorCode, String details) {
super(errorCode.getMessage() + " : " + details);
this.errorCode = errorCode;
this.details = details;
}
public PariException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.details = null;
}
// Getters
public ErrorCode getErrorCode() {
return errorCode;
}
public String getDetails() {
return details;
}
// Méthode utilitaire pour construire les messages
public static String buildLimiteMiseMessage(double limite) {
return String.format("Limite de mise dépassée (max %,.0f FCFA par course selon Article 2)", limite);
}
}

View File

@@ -1,68 +0,0 @@
package com.pmumali.simple.model;
import jakarta.persistence.*;
@Entity
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private int numero;
private boolean estNonPartant;
private Integer positionArrivee;
@ManyToOne(fetch = FetchType.LAZY)
private Course course;
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public boolean isEstNonPartant() {
return estNonPartant;
}
public void setEstNonPartant(boolean estNonPartant) {
this.estNonPartant = estNonPartant;
}
public Integer getPositionArrivee() {
return positionArrivee;
}
public void setPositionArrivee(Integer positionArrivee) {
this.positionArrivee = positionArrivee;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
}

View File

@@ -1,287 +0,0 @@
package com.pmumali.simple.model;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "clients")
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String nom;
@Column(nullable = false, length = 50)
private String prenom;
@Column(nullable = false, unique = true, length = 30)
private String numeroClient;
@Column(nullable = false, unique = true, length = 50)
private String email;
@Column(nullable = false, length = 20)
private String telephone;
@Column(name = "date_naissance")
private LocalDate dateNaissance;
@Column(nullable = false, length = 1)
private String sexe; // M ou F
@Column(name = "piece_identite", nullable = false, length = 50)
private String pieceIdentite; // Numéro de CNI/Passeport
@Column(name = "type_piece", length = 20)
private String typePieceIdentite; // CNI, PASSEPORT, PERMIS
@Column(name = "adresse_postale", length = 100)
private String adressePostale;
@Column(name = "code_postal", length = 10)
private String codePostal;
@Column(length = 50)
private String ville;
@Column(length = 50)
private String pays = "Mali";
@Column(name = "date_inscription", nullable = false)
private LocalDate dateInscription = LocalDate.now();
@Column(name = "est_verifie", nullable = false)
private boolean estVerifie = false;
@Column(name = "est_bloque", nullable = false)
private boolean estBloque = false;
@Column(name = "solde_compte", nullable = false)
private double soldeCompte = 0.0;
@Column(name = "limite_mise", nullable = false)
private double limiteMise = 10000.0; // 20 x 500 FCFA (Article 2)
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Pari> paris = new ArrayList<>();
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
private List<Transaction> transactions = new ArrayList<>();
// Constructeurs
public Client() {
}
public Client(String nom, String prenom, String numeroClient, String email, String telephone) {
this.nom = nom;
this.prenom = prenom;
this.numeroClient = numeroClient;
this.email = email;
this.telephone = telephone;
}
// Getters et Setters
public Long getId() {
return id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
public String getNumeroClient() {
return numeroClient;
}
public void setNumeroClient(String numeroClient) {
this.numeroClient = numeroClient;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
public LocalDate getDateNaissance() {
return dateNaissance;
}
public void setDateNaissance(LocalDate dateNaissance) {
this.dateNaissance = dateNaissance;
}
public String getSexe() {
return sexe;
}
public void setSexe(String sexe) {
this.sexe = sexe;
}
public String getPieceIdentite() {
return pieceIdentite;
}
public void setPieceIdentite(String pieceIdentite) {
this.pieceIdentite = pieceIdentite;
}
public String getTypePieceIdentite() {
return typePieceIdentite;
}
public void setTypePieceIdentite(String typePieceIdentite) {
this.typePieceIdentite = typePieceIdentite;
}
public String getAdressePostale() {
return adressePostale;
}
public void setAdressePostale(String adressePostale) {
this.adressePostale = adressePostale;
}
public String getCodePostal() {
return codePostal;
}
public void setCodePostal(String codePostal) {
this.codePostal = codePostal;
}
public String getVille() {
return ville;
}
public void setVille(String ville) {
this.ville = ville;
}
public String getPays() {
return pays;
}
public void setPays(String pays) {
this.pays = pays;
}
public LocalDate getDateInscription() {
return dateInscription;
}
public void setDateInscription(LocalDate dateInscription) {
this.dateInscription = dateInscription;
}
public boolean isEstVerifie() {
return estVerifie;
}
public void setEstVerifie(boolean estVerifie) {
this.estVerifie = estVerifie;
}
public boolean isEstBloque() {
return estBloque;
}
public void setEstBloque(boolean estBloque) {
this.estBloque = estBloque;
}
public double getSoldeCompte() {
return soldeCompte;
}
public void setSoldeCompte(double soldeCompte) {
this.soldeCompte = soldeCompte;
}
public double getLimiteMise() {
return limiteMise;
}
public void setLimiteMise(double limiteMise) {
this.limiteMise = limiteMise;
}
public List<Pari> getParis() {
return paris;
}
public void setParis(List<Pari> paris) {
this.paris = paris;
}
public List<Transaction> getTransactions() {
return transactions;
}
public void setTransactions(List<Transaction> transactions) {
this.transactions = transactions;
}
// Méthodes utilitaires
public void ajouterPari(Pari pari) {
paris.add(pari);
pari.setClient(this);
}
public void retirerPari(Pari pari) {
paris.remove(pari);
pari.setClient(null);
}
public void crediterCompte(double montant) {
this.soldeCompte += montant;
}
public void debiterCompte(double montant) {
if (this.soldeCompte < montant) {
throw new IllegalStateException("Solde insuffisant");
}
this.soldeCompte -= montant;
}
@Override
public String toString() {
return "Client{" +
"id=" + id +
", nom='" + nom + '\'' +
", prenom='" + prenom + '\'' +
", numeroClient='" + numeroClient + '\'' +
'}';
}
}

View File

@@ -1,35 +0,0 @@
package com.pmumali.simple.model;
public class Combinaison {
private final Cheval cheval1;
private final Cheval cheval2;
private int nombreMises;
public Combinaison(Cheval cheval1, Cheval cheval2) {
this.cheval1 = cheval1;
this.cheval2 = cheval2;
}
// Getters
public Cheval getCheval1() { return cheval1; }
public Cheval getCheval2() { return cheval2; }
public int getNombreMises() { return nombreMises; }
public void setNombreMises(int nombreMises) {
this.nombreMises = nombreMises;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Combinaison)) return false;
Combinaison that = (Combinaison) o;
return (cheval1.equals(that.cheval1) && cheval2.equals(that.cheval2)) ||
(cheval1.equals(that.cheval2) && cheval2.equals(that.cheval1));
}
@Override
public int hashCode() {
return cheval1.hashCode() + cheval2.hashCode(); // Ordre non important
}
}

View File

@@ -1,92 +0,0 @@
package com.pmumali.simple.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private LocalDateTime dateCourse;
private boolean estAnnulee;
private boolean deadHeat;
private boolean isTerminee=false;
public boolean isTerminee() {
return isTerminee;
}
public void setTerminee(boolean terminee) {
isTerminee = terminee;
}
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<Cheval> chevaux = new ArrayList<>();
@OneToMany(mappedBy = "course")
private List<Pari> paris = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public LocalDateTime getDateCourse() {
return dateCourse;
}
public void setDateCourse(LocalDateTime dateCourse) {
this.dateCourse = dateCourse;
}
public boolean isEstAnnulee() {
return estAnnulee;
}
public void setEstAnnulee(boolean estAnnulee) {
this.estAnnulee = estAnnulee;
}
public boolean isDeadHeat() {
return deadHeat;
}
public void setDeadHeat(boolean deadHeat) {
this.deadHeat = deadHeat;
}
public List<Cheval> getChevaux() {
return chevaux;
}
public void setChevaux(List<Cheval> chevaux) {
this.chevaux = chevaux;
}
public List<Pari> getParis() {
return paris;
}
public void setParis(List<Pari> paris) {
this.paris = paris;
}
}

View File

@@ -1,108 +0,0 @@
package com.pmumali.simple.model;
import com.pmumali.simple.model.enums.TypePari;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Pari {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double montantMise;
private LocalDateTime datePari;
private boolean estPaye;
private double gains;
@ManyToOne(fetch = FetchType.LAZY)
private Client client;
@ManyToOne(fetch = FetchType.LAZY)
private Course course;
@ManyToMany
@JoinTable(
name = "pari_cheval",
joinColumns = @JoinColumn(name = "pari_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> chevauxJumeles = new ArrayList<>();
public TypePari getTypePari() {
return typePari;
}
public void setTypePari(TypePari typePari) {
this.typePari = typePari;
}
@Enumerated(EnumType.STRING)
private TypePari typePari; // JUMELEC_GAGNANT ou JUMELEC_PLACE
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public double getMontantMise() {
return montantMise;
}
public void setMontantMise(double montantMise) {
this.montantMise = montantMise;
}
public LocalDateTime getDatePari() {
return datePari;
}
public void setDatePari(LocalDateTime datePari) {
this.datePari = datePari;
}
public boolean isEstPaye() {
return estPaye;
}
public void setEstPaye(boolean estPaye) {
this.estPaye = estPaye;
}
public double getGains() {
return gains;
}
public void setGains(double gains) {
this.gains = gains;
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
public List<Cheval> getChevauxJumeles() {
return chevauxJumeles;
}
public void setChevauxJumeles(List<Cheval> chevauxJumeles) {
this.chevauxJumeles = chevauxJumeles;
}
}

View File

@@ -1,192 +0,0 @@
package com.pmumali.simple.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.*;
@Entity
@Table(name = "resultats_courses")
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "course_id", nullable = false, unique = true)
private Course course;
@Column(name = "date_publication", nullable = false)
private LocalDateTime datePublication = LocalDateTime.now();
@Column(name = "est_officiel", nullable = false)
private boolean estOfficiel = false;
@Column(name = "masse_a_partager", nullable = false)
private double masseAPartager;
@Column(name = "tirelire_active", nullable = false)
private boolean tirelireActive = false;
@Column(name = "montant_tirelire")
private Double montantTirelire;
@ElementCollection
@CollectionTable(name = "resultats_combinaisons", joinColumns = @JoinColumn(name = "resultat_id"))
@MapKeyColumn(name = "combinaison")
@Column(name = "rapport")
private Map<String, Double> rapports = new HashMap<>();
@ElementCollection
@CollectionTable(name = "resultats_non_partants", joinColumns = @JoinColumn(name = "resultat_id"))
@Column(name = "cheval_id")
private Set<Long> chevauxNonPartants = new HashSet<>();
@Enumerated(EnumType.STRING)
@Column(name = "type_resultat", nullable = false)
private TypeResultat typeResultat;
@Version
private Long version;
// Enumération pour les types de résultats
public enum TypeResultat {
NORMAL,
DEAD_HEAT_PREMIERS,
DEAD_HEAT_DEUXIEMES,
DEAD_HEAT_TROISIEMES,
COURSE_ANNULEE
}
// Constructeurs
public ResultatCourse() {}
public ResultatCourse(Course course) {
this.course = course;
}
// Getters et Setters
public Long getId() {
return id;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
public LocalDateTime getDatePublication() {
return datePublication;
}
public void setDatePublication(LocalDateTime datePublication) {
this.datePublication = datePublication;
}
public boolean isEstOfficiel() {
return estOfficiel;
}
public void setEstOfficiel(boolean estOfficiel) {
this.estOfficiel = estOfficiel;
}
public double getMasseAPartager() {
return masseAPartager;
}
public void setMasseAPartager(double masseAPartager) {
this.masseAPartager = masseAPartager;
}
public boolean isTirelireActive() {
return tirelireActive;
}
public void setTirelireActive(boolean tirelireActive) {
this.tirelireActive = tirelireActive;
}
public Double getMontantTirelire() {
return montantTirelire;
}
public void setMontantTirelire(Double montantTirelire) {
this.montantTirelire = montantTirelire;
}
public Map<String, Double> getRapports() {
return rapports;
}
public void setRapports(Map<String, Double> rapports) {
this.rapports = rapports;
}
public Set<Long> getChevauxNonPartants() {
return chevauxNonPartants;
}
public void setChevauxNonPartants(Set<Long> chevauxNonPartants) {
this.chevauxNonPartants = chevauxNonPartants;
}
public TypeResultat getTypeResultat() {
return typeResultat;
}
public void setTypeResultat(TypeResultat typeResultat) {
this.typeResultat = typeResultat;
}
public Long getVersion() {
return version;
}
// Méthodes métiers
public void ajouterRapport(String combinaison, double rapport) {
this.rapports.put(combinaison, rapport);
}
public void marquerNonPartant(Long chevalId) {
this.chevauxNonPartants.add(chevalId);
}
public boolean estCombinaisonPayable(String combinaison) {
return this.rapports.containsKey(combinaison);
}
public void activerTirelire(double montant) {
this.tirelireActive = true;
this.montantTirelire = montant;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ResultatCourse)) return false;
ResultatCourse that = (ResultatCourse) o;
return Objects.equals(id, that.id) &&
Objects.equals(course, that.course);
}
@Override
public int hashCode() {
return Objects.hash(id, course);
}
@Override
public String toString() {
return "ResultatCourse{" +
"id=" + id +
", course=" + course.getId() +
", typeResultat=" + typeResultat +
", rapports=" + rapports.size() +
'}';
}
}

View File

@@ -1,33 +0,0 @@
package com.pmumali.simple.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "transactions")
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private double montant;
@Column(nullable = false)
private LocalDateTime dateHeure = LocalDateTime.now();
@Column(nullable = false, length = 20)
private String type; // DÉPÔT, RETRAIT, GAIN, REMBOURSEMENT
@Column(length = 100)
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private Client client;
// Getters, setters et constructeurs
}

View File

@@ -1,6 +0,0 @@
package com.pmumali.simple.model.enums;
public enum TypePari {
JUMELEC_GAGNANT,
JUMELEC_PLACE
}

View File

@@ -1,7 +0,0 @@
package com.pmumali.simple.repository;
import com.pmumali.simple.model.Cheval;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChevalRepository extends JpaRepository<Cheval,Long> {
}

View File

@@ -1,7 +0,0 @@
package com.pmumali.simple.repository;
import com.pmumali.simple.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CourseRepository extends JpaRepository<Course,Long> {
}

View File

@@ -1,72 +0,0 @@
package com.pmumali.simple.repository;
import com.pmumali.simple.model.Pari;
import com.pmumali.simple.model.Client;
import com.pmumali.simple.model.Course;
import com.pmumali.simple.model.enums.TypePari;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public interface PariRepository extends JpaRepository<Pari, Long> {
double sumByClientAndCourse(Long clientID, Long courseId);
// Trouver tous les paris d'un client
List<Pari> findByClient(Client client);
// Trouver les paris par course
List<Pari> findByCourse(Course course);
// Trouver les paris par type (JUMELEC_PLACE ou JUMELEC_GAGNANT)
List<Pari> findByTypePari(TypePari typePari);
// Somme des mises d'un client pour une course (Article 2 - Limitation des enjeux)
@Query("SELECT COALESCE(SUM(p.montantMise), 0) FROM Pari p WHERE p.client = :client AND p.course = :course")
double sumMisesByClientAndCourse(@Param("client") Client client, @Param("course") Course course);
// Trouver les paris gagnants pour une combinaison donnée
@Query("SELECT p FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course " +
"AND c.id IN (:cheval1Id, :cheval2Id) GROUP BY p HAVING COUNT(c) = 2")
List<Pari> findParisGagnantsForCombinaison(
@Param("course") Course course,
@Param("cheval1Id") Long cheval1Id,
@Param("cheval2Id") Long cheval2Id
);
// Nombre de mises pour une combinaison spécifique
@Query("SELECT COUNT(p) FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course " +
"AND c.id IN (:cheval1Id, :cheval2Id) GROUP BY p HAVING COUNT(c) = 2")
long countMisesForCombinaison(
@Param("course") Course course,
@Param("cheval1Id") Long cheval1Id,
@Param("cheval2Id") Long cheval2Id
);
// Trouver les paris contenant un cheval non-partant (Article 4)
@Query("SELECT DISTINCT p FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course AND c.estNonPartant = true")
List<Pari> findParisAvecNonPartants(@Param("course") Course course);
// Statistiques pour le dashboard
@Query("SELECT NEW map(p.typePari as type, COUNT(p) as nombre, SUM(p.montantMise) as totalMises) " +
"FROM Pari p WHERE p.datePari BETWEEN :start AND :end GROUP BY p.typePari")
List<Map<String, Object>> getStatistiquesParType(
@Param("start") LocalDateTime startDate,
@Param("end") LocalDateTime endDate
);
// Trouver les paris non payés pour une course
List<Pari> findByCourseAndEstPayeFalse(Course course);
// Méthode optimisée pour le calcul des rapports
@Query("SELECT NEW map(c1.id as cheval1Id, c2.id as cheval2Id, COUNT(p) as mises) " +
"FROM Pari p JOIN p.chevauxJumeles c1 JOIN p.chevauxJumeles c2 " +
"WHERE p.course = :course AND c1.id < c2.id " +
"GROUP BY c1.id, c2.id")
List<Map<String, Object>> getMisesParCombinaison(@Param("course") Course course);
}

View File

@@ -1,138 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,309 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -1,116 +0,0 @@
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)");
}
}
}