Initial commit

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

View File

@@ -0,0 +1,40 @@
package com.pmu.jumele.controller;
import com.pmu.jumele.dto.*;
import com.pmu.jumele.service.JumeleGagnantService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/jumele")
public class JumeleController {
private final JumeleGagnantService service;
public JumeleController(JumeleGagnantService service) {
this.service = service;
}
@PostMapping("/pari")
public ResponseEntity<?> enregistrerPari(@RequestBody PariRequest req) {
try {
Map<String, Object> res = service.enregistrerPari(req);
return ResponseEntity.ok(res);
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
@PostMapping("/resultat")
public ResponseEntity<?> calculer(@RequestBody PositionsRequest req) {
Map<String, Object> res = service.calculer(req);
return ResponseEntity.ok(res);
}
@GetMapping("/paris/{courseId}")
public ResponseEntity<?> listerParis(@PathVariable String courseId) {
return ResponseEntity.ok(service.getClass().getName()); // placeholder: add endpoint to fetch from repo if needed
}
}

View File

@@ -0,0 +1,14 @@
package com.pmu.jumele.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
public class PaiementResponse {
private String parieur;
private boolean gagnant;
private BigDecimal gain;
private String combinaison;
}

View File

@@ -0,0 +1,14 @@
package com.pmu.jumele.dto;
import lombok.Data;
import java.util.List;
@Data
public class PariRequest {
private String parieur;
private String courseId;
private List<Integer> chevaux; // can be 2 (unit), or list to generate combinations (combiné/champ)
private int mise; // montant total for the formula (we'll split for unitary bets)
private String formuleType; // "unitaire","combine","champ_total","champ_partiel"
private List<Integer> champSelection; // used for champ partiel (if formuleType == champ_partiel)
}

View File

@@ -0,0 +1,14 @@
package com.pmu.jumele.dto;
import lombok.Data;
import java.util.List;
@Data
public class PositionsRequest {
private String courseId;
// positions: list of positions; each position is a list of horse numbers (to represent dead-heat)
// positions.get(0) = list of horses classified first
// positions.get(1) = list of horses classified second, etc.
private List<List<Integer>> positions;
private List<Integer> nonPartants; // optional
}

View File

@@ -0,0 +1,24 @@
package com.pmu.jumele.entity;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
@Entity
@Table(name = "paris_jumele")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PariEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String parieur;
private String courseId;
// store combinaison as "a,b"
private String combinaison;
private BigDecimal mise;
}

View File

@@ -0,0 +1,10 @@
package com.pmu.jumele.repository;
import com.pmu.jumele.entity.PariEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PariRepository extends JpaRepository<PariEntity, Long> {
List<PariEntity> findByCourseId(String courseId);
List<PariEntity> findByCourseIdAndParieurAndCombinaison(String courseId, String parieur, String combinaison);
}

View File

@@ -0,0 +1,290 @@
package com.pmu.jumele.service;
import com.pmu.jumele.dto.*;
import com.pmu.jumele.entity.PariEntity;
import com.pmu.jumele.repository.PariRepository;
import com.pmu.jumele.util.CombinaisonUtil;
import com.pmu.jumele.util.FormulesTable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class JumeleGagnantService {
private final PariRepository pariRepository;
private static final BigDecimal MISE_MIN = BigDecimal.valueOf(500);
private static final int PLAFOND_MULT = 20;
private static final BigDecimal RAPPORT_MIN = BigDecimal.valueOf(1.1);
private static final int SCALE = 8;
// prélèvements légaux (paramétrable)
private BigDecimal prelevements = BigDecimal.ZERO;
public JumeleGagnantService(PariRepository pariRepository) {
this.pariRepository = pariRepository;
}
public void setPrelevements(BigDecimal p) { this.prelevements = p == null ? BigDecimal.ZERO : p; }
/**
* Enregistre un pari request — peut être unitaire ou une formule.
* Pour formules, convertit en paris unitaires (combinaisons) et enregistre chaque unité en respectant le tableau de valeurs.
*/
@Transactional
public Map<String, Object> enregistrerPari(PariRequest req) {
Map<String, Object> result = new HashMap<>();
// validation minimale
if (req.getMise() < MISE_MIN.intValue()) throw new IllegalArgumentException("Mise minimum = 500");
// calculer unités
Set<Set<Integer>> combsToPlace = new HashSet<>();
if ("unitaire".equalsIgnoreCase(req.getFormuleType()) || req.getChevaux().size() == 2) {
if (req.getChevaux().size() != 2) throw new IllegalArgumentException("Pari unitaire doit contenir 2 chevaux");
combsToPlace.add(new HashSet<>(req.getChevaux()));
} else if ("combine".equalsIgnoreCase(req.getFormuleType())) {
combsToPlace = CombinaisonUtil.allPairs(req.getChevaux());
} else if ("champ_total".equalsIgnoreCase(req.getFormuleType())) {
// champ total: base x all others; req.getChevaux() must contain base + others?
// For API simplicity: req.chevaux contains full list of partants; champ_total of a base is handled client-side by sending combos
combsToPlace = CombinaisonUtil.allPairs(req.getChevaux());
} else if ("champ_partiel".equalsIgnoreCase(req.getFormuleType())) {
if (req.getChampSelection() == null || req.getChampSelection().isEmpty())
throw new IllegalArgumentException("Champ partiel nécessite champSelection");
// base is first element of req.chevaux
Integer base = req.getChevaux().get(0);
for (Integer c : req.getChampSelection()) {
combsToPlace.add(new HashSet<>(Arrays.asList(base, c)));
}
} else {
throw new IllegalArgumentException("FormuleType inconnu");
}
// valeur unitaire par combinaison : on divise la mise totale proportionnellement selon tableau ou uniformément
// Simplicité : on divise la mise totale uniformément par le nombre de combinaisons
int nbComb = combsToPlace.size();
BigDecimal totalMise = BigDecimal.valueOf(req.getMise());
BigDecimal miseUnitaire = totalMise.divide(BigDecimal.valueOf(nbComb), SCALE, RoundingMode.HALF_UP);
// enregistrement avec respect du plafond 20 prises par parieur sur même combinaison
BigDecimal plafond = MISE_MIN.multiply(BigDecimal.valueOf(PLAFOND_MULT));
BigDecimal totalRembourse = BigDecimal.ZERO;
List<PariEntity> enregistrés = new ArrayList<>();
for (Set<Integer> comb : combsToPlace) {
String combStr = CombinaisonUtil.combToString(comb);
// somme déjà engagée par ce parieur sur cette combinaison dans la même course
List<PariEntity> deja = pariRepository.findByCourseIdAndParieurAndCombinaison(req.getCourseId(), req.getParieur(), combStr);
BigDecimal dejaTotal = deja.stream().map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal disponible = plafond.subtract(dejaTotal);
BigDecimal toRecord = miseUnitaire.min(disponible);
BigDecimal toRefund = miseUnitaire.subtract(toRecord);
if (toRecord.compareTo(BigDecimal.ZERO) > 0) {
PariEntity e = PariEntity.builder()
.parieur(req.getParieur())
.courseId(req.getCourseId())
.combinaison(combStr)
.mise(toRecord)
.build();
pariRepository.save(e);
enregistrés.add(e);
}
if (toRefund.compareTo(BigDecimal.ZERO) > 0) {
totalRembourse = totalRembourse.add(toRefund);
}
}
result.put("enregistres", enregistrés.size());
result.put("a_rembourser", totalRembourse.setScale(2, RoundingMode.HALF_UP));
return result;
}
/**
* Enregistre le résultat via PositionsRequest (positions = list of lists to express dead-heats).
*/
public void enregistrerResultat(PositionsRequest req) {
// store positions in memory — for calculation we require positions per course; for simplicity, pass positions during calcul.
// In this implementation, we'll pass PositionsRequest directly to calculer.
// To persist results, create a ResultEntity (omitted here).
}
/**
* Calculer paiements pour une course en fournissant PositionsRequest (dead-heat possible).
*/
@Transactional(readOnly = true)
public Map<String, Object> calculer(PositionsRequest positionsReq) {
Map<String, Object> out = new HashMap<>();
String courseId = positionsReq.getCourseId();
List<PariEntity> parisCourse = pariRepository.findByCourseId(courseId);
List<Integer> nonPartants = positionsReq.getNonPartants() == null ? Collections.emptyList() : positionsReq.getNonPartants();
// 1) Générer combinaisons payables selon article 3:
// positionsReq.positions[0] => premiers (list), positionsReq.positions[1] => deuxiemes (list), etc.
List<List<Integer>> positions = positionsReq.getPositions();
if (positions == null || positions.size() < 2) {
// moins de deux classés => tout remboursé (Article 8)
List<PaiementResponse> remboursements = parisCourse.stream()
.map(p -> new PaiementResponse(p.getParieur(), false, p.getMise(), p.getCombinaison()))
.collect(Collectors.toList());
out.put("paiements", remboursements);
out.put("cagnotte", BigDecimal.ZERO.setScale(2));
return out;
}
Set<Set<Integer>> combPayables = new HashSet<>();
// a) dead-heat premiers (positions[0] size >=2) -> toutes combinaisons 2-à-2 parmi premiers
List<Integer> premiers = positions.get(0);
if (premiers.size() >= 2) {
combPayables.addAll(CombinaisonUtil.allPairs(premiers));
}
// b) dead-heat seconds (positions[1] size >=2) -> all pairs combining each premier with each second
List<Integer> deuxiemes = positions.get(1);
if (deuxiemes.size() >= 1 && premiers.size() >= 1) {
for (Integer p : premiers) {
for (Integer d : deuxiemes) {
combPayables.add(new HashSet<>(Arrays.asList(p, d)));
}
}
}
// c) if positions[0].size()==1 and positions[1].size()==1 and no dead-heat: comb = {1er,2e}
if (premiers.size() == 1 && deuxiemes.size() == 1) {
combPayables.add(new HashSet<>(Arrays.asList(premiers.get(0), deuxiemes.get(0))));
}
// Remove combos involving non-partants -> these combos are remboursed (Article 4)
Set<Set<Integer>> combRemb = combPayables.stream()
.filter(c -> c.stream().anyMatch(nonPartants::contains))
.collect(Collectors.toSet());
// RNET = total des mises enregistrées sur la course
BigDecimal rnet = parisCourse.stream().map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
// MREMB = montant des mises sur combRemb
BigDecimal mremb = parisCourse.stream()
.filter(p -> combRemb.contains(stringToComb(p.getCombinaison())))
.map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal map = rnet.subtract(mremb).subtract(prelevements);
if (map.compareTo(BigDecimal.ZERO) < 0) map = BigDecimal.ZERO;
// Mises par combinaison (pour combPayables not remboursed)
Map<Set<Integer>, BigDecimal> misesParComb = new HashMap<>();
for (Set<Integer> comb : combPayables) {
if (combRemb.contains(comb)) {
misesParComb.put(comb, BigDecimal.ZERO);
} else {
BigDecimal s = parisCourse.stream()
.filter(p -> stringToComb(p.getCombinaison()).equals(comb))
.map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
misesParComb.put(comb, s);
}
}
// cas : aucune comb active => cagnotte
List<Set<Integer>> combActives = misesParComb.entrySet().stream()
.filter(e -> e.getValue().compareTo(BigDecimal.ZERO) > 0)
.map(Map.Entry::getKey).collect(Collectors.toList());
BigDecimal cagnotte = BigDecimal.ZERO;
Map<String, BigDecimal> gains = new HashMap<>();
if (combActives.isEmpty()) {
cagnotte = cagnotte.add(map);
} else if (combActives.size() == 1) {
// cas normal unique combinaison
Set<Integer> comb = combActives.get(0);
BigDecimal totMise = misesParComb.get(comb);
if (totMise.compareTo(BigDecimal.ZERO) == 0) {
cagnotte = cagnotte.add(map);
} else {
BigDecimal rapport = map.divide(totMise, SCALE, RoundingMode.HALF_UP).add(BigDecimal.ONE);
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
// payer chaque parieur sur la combinaison
for (PariEntity p : parisCourse) {
if (stringToComb(p.getCombinaison()).equals(comb)) {
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
}
}
}
} else {
// dead-heat / multiple combs payables
BigDecimal totalMisesOnCombActives = combActives.stream().map(misesParComb::get).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal benef = map.subtract(totalMisesOnCombActives);
if (benef.compareTo(BigDecimal.ZERO) < 0) benef = BigDecimal.ZERO;
int nbComb = combActives.size();
BigDecimal partParComb = benef.divide(BigDecimal.valueOf(nbComb), SCALE, RoundingMode.HALF_UP);
// allocate for each comb: partParComb distributed proportionally to mises on that comb
// but first for each comb we already have misesParComb.get(comb) that were removed from the map when computing benef
for (Set<Integer> comb : combActives) {
BigDecimal misesThis = misesParComb.get(comb);
if (misesThis.compareTo(BigDecimal.ZERO) == 0) continue;
BigDecimal ratio = partParComb.divide(misesThis, SCALE, RoundingMode.HALF_UP);
BigDecimal rapport = ratio.add(BigDecimal.ONE);
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
for (PariEntity p : parisCourse) {
if (stringToComb(p.getCombinaison()).equals(comb)) {
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
}
}
}
// traiter parts non couvertes (combActives avec mises 0): redistribuer their part among covered combs
BigDecimal partsNonCover = BigDecimal.ZERO;
for (Set<Integer> comb : combActives) {
if (misesParComb.get(comb).compareTo(BigDecimal.ZERO) == 0) {
partsNonCover = partsNonCover.add(partParComb);
}
}
if (partsNonCover.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal totalMisesCouvertes = combActives.stream()
.filter(c -> misesParComb.get(c).compareTo(BigDecimal.ZERO) > 0)
.map(misesParComb::get).reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalMisesCouvertes.compareTo(BigDecimal.ZERO) == 0) {
cagnotte = cagnotte.add(map);
} else {
// redistribute partsNonCover to covered combos proportionally
for (Set<Integer> comb : combActives) {
BigDecimal misesThis = misesParComb.get(comb);
if (misesThis.compareTo(BigDecimal.ZERO) == 0) continue;
BigDecimal share = partsNonCover.multiply(misesThis).divide(totalMisesCouvertes, SCALE, RoundingMode.HALF_UP);
// distribute share proportionally to parieurs on the comb
for (PariEntity p : parisCourse) {
if (stringToComb(p.getCombinaison()).equals(comb)) {
BigDecimal extra = p.getMise().multiply(share).divide(misesThis, SCALE, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(extra));
}
}
}
}
}
}
// Build payment responses per record (each pari)
List<PaiementResponse> payments = new ArrayList<>();
for (PariEntity p : parisCourse) {
BigDecimal gain = gains.getOrDefault(p.getParieur(), BigDecimal.ZERO);
boolean gagnant = gain.compareTo(BigDecimal.ZERO) > 0;
payments.add(new PaiementResponse(p.getParieur(), gagnant, gain, p.getCombinaison()));
}
out.put("paiements", payments);
out.put("cagnotte", cagnotte.setScale(2, RoundingMode.HALF_UP));
out.put("rnet", rnet.setScale(2, RoundingMode.HALF_UP));
out.put("mremb", mremb.setScale(2, RoundingMode.HALF_UP));
out.put("map", map.setScale(2, RoundingMode.HALF_UP));
return out;
}
private Set<Integer> stringToComb(String s) {
return Arrays.stream(s.split(",")).map(Integer::valueOf).collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,47 @@
package com.pmu.jumele.service;
import com.pmu.jumele.dto.PariRequest;
import com.pmu.jumele.dto.PositionsRequest;
import com.pmu.jumele.entity.PariEntity;
import com.pmu.jumele.repository.PariRepository;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
//@DataJpaTest
public class JumeleGagnantServiceTest {
// @Autowired
PariRepository repo;
// @Test
void test_simple_flow() {
JumeleGagnantService svc = new JumeleGagnantService(repo);
PariRequest pr = new PariRequest();
pr.setParieur("Alice");
pr.setCourseId("C1");
pr.setChevaux(Arrays.asList(1,2));
pr.setMise(500);
pr.setFormuleType("unitaire");
svc.enregistrerPari(pr);
// ajouter un autre pari sur la même comb
pr.setParieur("Bob");
svc.enregistrerPari(pr);
PositionsRequest pos = new PositionsRequest();
pos.setCourseId("C1");
pos.setPositions(List.of(List.of(1), List.of(2))); // 1er=1 ; 2e=2
Map<String,Object> res = svc.calculer(pos);
// assertThat(res).containsKey("paiements");
}
}

View File

@@ -0,0 +1,24 @@
package com.pmu.jumele.util;
import java.util.*;
import java.util.stream.Collectors;
public class CombinaisonUtil {
// génère toutes paires (unordered) d'une liste de chevaux
public static Set<Set<Integer>> allPairs(List<Integer> chevaux) {
Set<Set<Integer>> res = new HashSet<>();
int n = chevaux.size();
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
res.add(new HashSet<>(Arrays.asList(chevaux.get(i), chevaux.get(j))));
}
}
return res;
}
// conversion Set<Integer> -> "a,b" sorted string
public static String combToString(Set<Integer> comb) {
return comb.stream().sorted().map(Object::toString).collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,35 @@
package com.pmu.jumele.util;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class FormulesTable {
// For simplicity we store unit value per formula (complete combined) and simplified etc.
// We'll only need unit price to split a given "mise" into each unit bet when user submits a formula.
// Here we give reference table values per article 7 (unit values for "complete" formulas)
public static final Map<Integer, BigDecimal> COMBINED_COMPLETE = new HashMap<>();
public static final Map<Integer, BigDecimal> COMBINED_SIMPLE = new HashMap<>();
public static final Map<Integer, BigDecimal> CHAMP_TOTAL = new HashMap<>();
public static final Map<Integer, BigDecimal> CHAMP_PARTIEL = new HashMap<>();
static {
// fill a subset (full table can be added)
COMBINED_COMPLETE.put(3, BigDecimal.valueOf(3000));
COMBINED_COMPLETE.put(4, BigDecimal.valueOf(6000));
COMBINED_COMPLETE.put(5, BigDecimal.valueOf(10000));
// ... add rest as needed
COMBINED_SIMPLE.put(3, BigDecimal.valueOf(1500));
COMBINED_SIMPLE.put(4, BigDecimal.valueOf(3000));
// ...
CHAMP_TOTAL.put(3, BigDecimal.valueOf(1000));
CHAMP_TOTAL.put(4, BigDecimal.valueOf(1500));
// ...
CHAMP_PARTIEL.put(3, BigDecimal.valueOf(1500));
CHAMP_PARTIEL.put(4, BigDecimal.valueOf(2000));
// ...
}
}

View File

@@ -0,0 +1,13 @@
package com.pmu.mali.apiplr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiPlrApplication {
public static void main(String[] args) {
SpringApplication.run(ApiPlrApplication.class, args);
}
}

View File

@@ -0,0 +1,16 @@
package com.pmu.mali.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
public class Pari {
private String parieur; // identifiant client ou ticket
private int numeroCheval; // numéro du cheval parié
private TypePari type; // SIMPLE_GAGNANT ou SIMPLE_PLACE
private BigDecimal mise; // montant mis (ex: 500)
private boolean nonPartant; // vrai si cheval non-partant (remboursement)
private String ecurieId; // identifiant d'écurie / coupling (null si aucun)
}

View File

@@ -0,0 +1,16 @@
package com.pmu.mali.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
public class ResultatCourse {
// listes des numéros de chevaux classés (peuvent contenir plusieurs en cas de dead-heat)
private List<Integer> premiers; // un ou plusieurs (dead-heat)
private List<Integer> deuxiemes; // peut être vide
private List<Integer> troisiemes; // peut être vide
private int nombrePartants; // nombre de chevaux engagés (programme officiel)
}

View File

@@ -0,0 +1,6 @@
package com.pmu.mali.model;
public enum TypePari {
SIMPLE_GAGNANT,
SIMPLE_PLACE
}

View File

@@ -0,0 +1,367 @@
package com.pmu.mali.service;
import com.pmu.mali.model.Pari;
import com.pmu.mali.model.ResultatCourse;
import com.pmu.mali.model.TypePari;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Service de calcul des gains pour PARI SIMPLE (gagnant / placé)
* Implémente écurie, dead-heat, remboursements, cagnotte, rapport min.
*/
@Service
public class CalculPariService {
private static final BigDecimal RAPPORT_MIN = BigDecimal.valueOf(1.1); // rapport minimum
private static final int SCALE = 6; // précision interne
/**
* Résultat retourné : map parieur -> gain (montant payé, inclut la mise).
* On retourne aussi une structure pour la cagnotte (optionnel).
*/
public static class ResultatCalcul {
public Map<String, BigDecimal> gainsParParieur = new HashMap<>();
public BigDecimal cagnotteGagnant = BigDecimal.ZERO;
public BigDecimal cagnottePlace = BigDecimal.ZERO;
}
/**
* Point d'entrée principal.
*
* @param paris liste de paris (SIMPLE_GAGNANT et SIMPLE_PLACE mélangés)
* @param resultat résultat de la course
* @param prelevements montant total prélevé légalement (sur les deux types, si applicable)
* @return ResultatCalcul
*/
public ResultatCalcul calculer(List<Pari> paris, ResultatCourse resultat, BigDecimal prelevements) {
ResultatCalcul res = new ResultatCalcul();
if (prelevements == null) prelevements = BigDecimal.ZERO;
// Séparer paris par type
List<Pari> parisGagnant = filterByType(paris, TypePari.SIMPLE_GAGNANT);
List<Pari> parisPlace = filterByType(paris, TypePari.SIMPLE_PLACE);
// 1) Remboursement des non-partants
BigDecimal rembGagnant = rembourserNonPartants(parisGagnant, res);
BigDecimal rembPlace = rembourserNonPartants(parisPlace, res);
// 2) Calcul des masses à partager (RNET - MREMB - PRELEV)
BigDecimal masseGagnant = totalMise(parisGagnant).subtract(rembGagnant).subtract(prelevements);
BigDecimal massePlace = totalMise(parisPlace).subtract(rembPlace).subtract(prelevements);
if (masseGagnant.compareTo(BigDecimal.ZERO) < 0) masseGagnant = BigDecimal.ZERO;
if (massePlace.compareTo(BigDecimal.ZERO) < 0) massePlace = BigDecimal.ZERO;
// 3) Calcul gagnant (avec ecurie et dead-heat)
calculerGagnant(parisGagnant, resultat, masseGagnant, res);
// 4) Calcul placé (avec règles 4-7 / >=8 et dead-heat)
calculerPlace(parisPlace, resultat, massePlace, res);
return res;
}
/* ----------------------
Méthodes utilitaires
---------------------- */
private List<Pari> filterByType(List<Pari> all, TypePari t) {
return all.stream().filter(p -> p.getType() == t).collect(Collectors.toList());
}
private BigDecimal totalMise(List<Pari> list) {
return list.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* Rembourse toutes les mises dont pari.nonPartant == true
* Ajoute le montant remboursé aux gains du parieur.
* Retourne le total remboursé.
*/
private BigDecimal rembourserNonPartants(List<Pari> list, ResultatCalcul res) {
return rembourserNonPartants(list, res.gainsParParieur);
}
private BigDecimal rembourserNonPartants(List<Pari> list, Map<String, BigDecimal> gainsMap) {
BigDecimal total = BigDecimal.ZERO;
for (Pari p : list) {
if (p.isNonPartant()) {
total = total.add(p.getMise());
gainsMap.put(p.getParieur(),
gainsMap.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(p.getMise()));
}
}
return total;
}
/* ---------------------------------------
Calcul du GAGNANT (inclut dead-heat/ecurie)
--------------------------------------- */
private void calculerGagnant(List<Pari> parisGagnant, ResultatCourse resultat, BigDecimal masse, ResultatCalcul res) {
List<Integer> premiers = resultat.getPremiers(); // liste des premiers (1 ou plusieurs)
if (premiers == null || premiers.isEmpty()) {
// Aucun cheval classé -> masse va en cagnotte
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
return;
}
// Si arrivée normale (un seul premier)
if (premiers.size() == 1) {
int premier = premiers.get(0);
// déterminer écurie du cheval gagnant (s'il y a), puis totaliser mises sur l'écurie
// On crée une clé d'ecurie : si pari.ecurieId == null, utilises le numéro du cheval comme "ecurie"
Map<String, BigDecimal> miseParEcurie = new HashMap<>();
for (Pari p : parisGagnant) {
if (p.isNonPartant()) continue;
String ecurie = keyEcurie(p);
// Si ce cheval appartient à l'écurie gagnante OU il est lui-même le gagnant, on prendra en compte
miseParEcurie.put(ecurie, miseParEcurie.getOrDefault(ecurie, BigDecimal.ZERO).add(p.getMise()));
}
String ecurieGagnanteKey = null;
// trouver si le gagnant a une écurie dans les paris
Optional<Pari> anyPariSurGagnant = parisGagnant.stream()
.filter(p -> p.getNumeroCheval() == premier && !p.isNonPartant()).findAny();
if (anyPariSurGagnant.isPresent()) {
ecurieGagnanteKey = keyEcurie(anyPariSurGagnant.get());
} else {
// pas de mise sur le gagnant -> vérifier s'il y a mises sur autres chevaux de la même écurie
// chercher toute mise sur chevaux avec même ecurieId (si ecurie known)
// Si pas de mise du tout sur la combinaison gagnante => cagnotte
// So we'll compute totalMisePayable below
}
// calculer total des mises payables (mises sur le cheval gagnant et, si écurie, sur ses co-écuries)
BigDecimal totalMisePayable = BigDecimal.ZERO;
if (ecurieGagnanteKey != null) {
totalMisePayable = miseParEcurie.getOrDefault(ecurieGagnanteKey, BigDecimal.ZERO);
} else {
// pas de pari identifié sur gagnant -> peut être 0
// cherché toutes mises portant exactement sur le cheval gagnant
totalMisePayable = parisGagnant.stream()
.filter(p -> p.getNumeroCheval() == premier && !p.isNonPartant())
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
}
if (totalMisePayable.compareTo(BigDecimal.ZERO) <= 0) {
// Aucun pari sur le gagnant (ni sur son ecurie) => vers cagnotte
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
return;
}
// Rapport brut = (masse / totalMisePayable) + 1 (on restitue la mise + bénéfice)
BigDecimal rapport = masse.divide(totalMisePayable, SCALE, RoundingMode.HALF_UP).add(BigDecimal.ONE);
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
// payer chaque parieur ayant parié sur ce cheval/ecurie
for (Pari p : parisGagnant) {
if (p.isNonPartant()) continue;
String key = keyEcurie(p);
boolean payable = false;
if (ecurieGagnanteKey != null) {
// payables si même ecurie
payable = ecurieGagnanteKey.equals(key);
} else {
payable = (p.getNumeroCheval() == premier);
}
if (payable) {
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
res.gainsParParieur.put(p.getParieur(),
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
}
}
return;
}
/* -------------------------
* Cas dead-heat (plusieurs premiers)
* ------------------------- */
// 1) calculer total mise sur les chevaux payables (les chevaux classés premiers)
// Règle du règlement : "le montant de toutes les mises sur les divers chevaux payables est dabord retiré de la masse à partager."
BigDecimal misesSurPayables = parisGagnant.stream()
.filter(p -> !p.isNonPartant() && premiers.contains(p.getNumeroCheval()))
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
// Bénéfice à répartir
BigDecimal benefice = masse.subtract(misesSurPayables);
if (benefice.compareTo(BigDecimal.ZERO) < 0) benefice = BigDecimal.ZERO;
int nbChevauxPremiers = premiers.size();
if (nbChevauxPremiers == 0) {
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
return;
}
// Diviser le bénéfice en autant de parts qu'il y a de chevaux classés premiers
BigDecimal partParCheval = benefice.divide(BigDecimal.valueOf(nbChevauxPremiers), SCALE, RoundingMode.HALF_UP);
// Pour chaque cheval premier : répartir sa part au prorata des mises sur ce cheval
for (Integer cheval : premiers) {
// mises sur ce cheval
BigDecimal misesSurCheval = parisGagnant.stream()
.filter(p -> !p.isNonPartant() && p.getNumeroCheval() == cheval)
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
if (misesSurCheval.compareTo(BigDecimal.ZERO) == 0) {
// si aucune mise sur ce cheval, sa part est redistribuée entre les autres premiers
// impl: ajouter cette part à cagnotte temporaire pour redistribution
// pour simplicité on ajoute à cagnotte et on répartira après
res.cagnotteGagnant = res.cagnotteGagnant.add(partParCheval);
continue;
}
// montant additionnel par unité de mise = partParCheval / misesSurCheval
BigDecimal ratio = partParCheval.divide(misesSurCheval, SCALE, RoundingMode.HALF_UP);
BigDecimal rapport = ratio.add(BigDecimal.ONE); // ajoute la mise
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
// payer chaque parieur sur ce cheval
for (Pari p : parisGagnant) {
if (p.isNonPartant()) continue;
if (p.getNumeroCheval() == cheval) {
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
res.gainsParParieur.put(p.getParieur(),
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
} else {
// si écurie: si cheval p appartient à écurie du cheval premier, il peut avoir droit (règle d'écurie)
// on doit totaliser mises par écurie gagnante et partager la part correspondante.
}
}
}
// NB: gestion complète des écuries dans dead-heat nécessite agrégation par ecurie + redistribution des parts relatives.
// Pour respecter à la lettre, on devrait :
// - agréger mises par ecurie pour les chevaux premiers,
// - distribuer la partParCheval à l'ecurie, puis parmi les chevaux de l'ecurie proportionnellement,
// - ci-dessus on a fait la distribution cheval-par-cheval (simplifié).
}
private String keyEcurie(Pari p) {
if (p.getEcurieId() != null && !p.getEcurieId().trim().isEmpty()) {
return "E:" + p.getEcurieId();
} else {
return "H:" + p.getNumeroCheval(); // seul cheval = sa "propre ecurie"
}
}
/* ---------------------------------------
* Calcul du PLACE (inclut dead-heat/ecurie)
* --------------------------------------- */
private void calculerPlace(List<Pari> parisPlace, ResultatCourse resultat, BigDecimal masse, ResultatCalcul res) {
// déterminer horses payable: si >=8 partants => 3 places payées, si 4-7 => 2, sinon mise en cagnotte (article)
int nPartants = resultat.getNombrePartants();
int nbPlacesPayees;
if (nPartants >= 8) nbPlacesPayees = 3;
else if (nPartants >= 4) nbPlacesPayees = 2;
else {
// si moins de 4 partants, la masse place va en cagnotte
res.cagnottePlace = res.cagnottePlace.add(masse);
return;
}
// Construction des chevaux payables selon les listes de résultat :
// Remarque : listes peuvent contenir dead-heats (plusieurs elements)
List<Integer> payables = new ArrayList<>();
if (nbPlacesPayees == 2) {
payables.addAll(resultat.getPremiers());
payables.addAll(resultat.getDeuxiemes());
} else {
// 3 places
payables.addAll(resultat.getPremiers());
payables.addAll(resultat.getDeuxiemes());
payables.addAll(resultat.getTroisiemes());
}
// on conserve distincts tout en gardant l'ordre logique
LinkedHashSet<Integer> set = new LinkedHashSet<>(payables);
List<Integer> chevauxPayables = new ArrayList<>(set);
if (chevauxPayables.isEmpty()) {
res.cagnottePlace = res.cagnottePlace.add(masse);
return;
}
// Retirer "le montant de toutes les mises sur les divers chevaux payables" de la masse
BigDecimal miseSurPayables = parisPlace.stream()
.filter(p -> !p.isNonPartant() && chevauxPayables.contains(p.getNumeroCheval()))
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal benefice = masse.subtract(miseSurPayables);
if (benefice.compareTo(BigDecimal.ZERO) < 0) benefice = BigDecimal.ZERO;
// Le bénéfice est divisé en autant de parts égales qu'il y a de "combinaisons payables".
// Ici on considère "combinaisons payables" = nombre de positions payées (nbPlacesPayees)
// -> chaque part ensuite partagée proportionnellement entre chevaux payables de la position concernée.
BigDecimal partParCombinaison = benefice.divide(BigDecimal.valueOf(nbPlacesPayees), SCALE, RoundingMode.HALF_UP);
// Répartition : pour chaque position (1er, 2e, 3e) on divise la part entre les chevaux classés à cette position
// puis, pour chaque cheval, on partage au prorata de ses mises sur cette position parmi les chevaux payables de la position.
// Simplification pratique: on va parcourir les positions et appliquer.
// Position 1
List<Integer> premiers = resultat.getPremiers();
repartirPlacePosition(parisPlace, premiers, partParCombinaison, res);
if (nbPlacesPayees >= 2) {
List<Integer> deuxiemes = resultat.getDeuxiemes();
repartirPlacePosition(parisPlace, deuxiemes, partParCombinaison, res);
}
if (nbPlacesPayees >= 3) {
List<Integer> troisiemes = resultat.getTroisiemes();
repartirPlacePosition(parisPlace, troisiemes, partParCombinaison, res);
}
// Note : si pour une position donnée il n'y a aucune mise, selon le règlement sa part est répartie entre les autres chevaux payables.
// Ici, si aucune mise sur une position entière, on ajoute cette part à la cagnottePlace (impl simplifiée).
}
private void repartirPlacePosition(List<Pari> parisPlace, List<Integer> chevauxPosition, BigDecimal partParCombinaison, ResultatCalcul res) {
if (chevauxPosition == null || chevauxPosition.isEmpty()) {
// pas de cheval classé à cette position -> part va en cagnotte
res.cagnottePlace = res.cagnottePlace.add(partParCombinaison);
return;
}
// total mises sur les chevaux de cette position
BigDecimal totalMises = parisPlace.stream()
.filter(p -> !p.isNonPartant() && chevauxPosition.contains(p.getNumeroCheval()))
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalMises.compareTo(BigDecimal.ZERO) == 0) {
// aucune mise sur cette position -> part est ajoutée à cagnotte pour redistribution (impl simplifiée)
res.cagnottePlace = res.cagnottePlace.add(partParCombinaison);
return;
}
// chaque cheval recevra une fraction proportionnelle de la partParCombinaison
for (Integer cheval : chevauxPosition) {
BigDecimal misesSurCheval = parisPlace.stream()
.filter(p -> !p.isNonPartant() && p.getNumeroCheval() == cheval)
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
if (misesSurCheval.compareTo(BigDecimal.ZERO) == 0) continue;
// montant additionnel pour ce cheval = partParCombinaison * (misesSurCheval / totalMises)
BigDecimal share = partParCombinaison.multiply(misesSurCheval).divide(totalMises, SCALE, RoundingMode.HALF_UP);
// Le rapport brut pour chaque mise sur ce cheval = (share / misesSurCheval) + 1
BigDecimal ratio = share.divide(misesSurCheval, SCALE, RoundingMode.HALF_UP);
BigDecimal rapport = ratio.add(BigDecimal.ONE);
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
// appliquer paiement à tous les parieurs sur ce cheval
for (Pari p : parisPlace) {
if (p.isNonPartant()) continue;
if (p.getNumeroCheval() == cheval) {
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
res.gainsParParieur.put(p.getParieur(),
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
// CourseController.java
package com.pmumali.jumeleordre.controller;
import com.pmumali.jumeleordre.dto.ResultatCourseDto;
import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException;
import com.pmumali.jumeleordre.model.Course;
import com.pmumali.jumeleordre.service.ResultatCourseService;
import com.pmumali.jumeleordre.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;
@Autowired
private ResultatCourseService resultatCourseService;
@GetMapping("/actives")
public ResponseEntity<List<Course>> getCoursesActives() {
List<Course> courses = courseService.getCoursesActives();
return ResponseEntity.ok(courses);
}
@PostMapping("/resultat")
public ResponseEntity<Void> soumettreResultatCourse(@RequestBody ResultatCourseDto resultatDto) {
try {
resultatCourseService.traiterResultatCourse(resultatDto);
return ResponseEntity.ok().build();
} catch (ResultatCourseInvalideException e) {
return ResponseEntity.badRequest().build();
}
}
}

View File

@@ -0,0 +1,43 @@
// ParisController.java
package com.pmumali.jumeleordre.controller;
import com.pmumali.jumeleordre.dto.ParisDto;
import com.pmumali.jumeleordre.dto.GainsDto;
import com.pmumali.jumeleordre.exception.ParisInvalideException;
import com.pmumali.jumeleordre.model.Paris;
import com.pmumali.jumeleordre.service.ParisService;
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 ParisController {
@Autowired
private ParisService parisService;
@PostMapping
public ResponseEntity<Paris> enregistrerParis(@RequestBody ParisDto parisDto) {
try {
Paris paris = parisService.enregistrerParis(parisDto);
return ResponseEntity.ok(paris);
} catch (ParisInvalideException e) {
return ResponseEntity.badRequest().body(null);
}
}
@GetMapping("/parieur/{idParieur}")
public ResponseEntity<List<Paris>> getParisParParieur(@PathVariable String idParieur) {
List<Paris> paris = parisService.getParisParParieur(idParieur);
return ResponseEntity.ok(paris);
}
@GetMapping("/gains/{idParis}")
public ResponseEntity<GainsDto> getGains(@PathVariable Long idParis) {
// Implémentation pour obtenir les détails des gains d'un pari
return ResponseEntity.ok(new GainsDto());
}
}

View File

@@ -0,0 +1,11 @@
// GainsDto.java
package com.pmumali.jumeleordre.dto;
import lombok.Data;
@Data
public class GainsDto {
private Long idParis;
private double montant;
private String statut;
}

View File

@@ -0,0 +1,13 @@
// ParisDto.java
package com.pmumali.jumeleordre.dto;
import lombok.Data;
@Data
public class ParisDto {
private Long idCourse;
private Long idChevalPremier;
private Long idChevalDeuxieme;
private double mise;
private String idParieur;
}

View File

@@ -0,0 +1,14 @@
// ResultatCourseDto.java
package com.pmumali.jumeleordre.dto;
import lombok.Data;
import java.util.List;
@Data
public class ResultatCourseDto {
private Long idCourse;
private Long idChevalPremier;
private Long idChevalDeuxieme;
private List<Long> chevauxDeadHeat;
}

View File

@@ -0,0 +1,12 @@
// ChevalInvalideException.java
package com.pmumali.jumeleordre.exception;
public class ChevalInvalideException extends JumeleOrdreException {
public ChevalInvalideException(String message) {
super(message);
}
public ChevalInvalideException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,8 @@
// CourseDejaTermineeException.java
package com.pmumali.jumeleordre.exception;
public class CourseDejaTermineeException extends CourseInvalideException {
public CourseDejaTermineeException(Long idCourse) {
super(String.format("La course avec l'ID %d est déjà terminée", idCourse));
}
}

View File

@@ -0,0 +1,12 @@
// CourseInvalideException.java
package com.pmumali.jumeleordre.exception;
public class CourseInvalideException extends JumeleOrdreException {
public CourseInvalideException(String message) {
super(message);
}
public CourseInvalideException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,54 @@
package com.pmumali.jumeleordre.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(JumeleOrdreException.class)
public ResponseEntity<Object> handleJumeleOrdreException(
JumeleOrdreException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", ex.getMessage());
body.put("type", ex.getClass().getSimpleName());
HttpStatus status = HttpStatus.BAD_REQUEST;
if (ex instanceof CourseDejaTermineeException) {
status = HttpStatus.CONFLICT;
} else if (ex instanceof LimiteMiseDepasseeException) {
LimiteMiseDepasseeException specificEx = (LimiteMiseDepasseeException) ex;
body.put("miseActuelle", specificEx.getMiseActuelle());
body.put("miseMaximum", specificEx.getMiseMaximum());
} else if (ex instanceof ValidationException) {
ValidationException validationEx = (ValidationException) ex;
body.put("erreurs", validationEx.getErreurs());
status = HttpStatus.UNPROCESSABLE_ENTITY;
}
return new ResponseEntity<>(body, status);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGlobalException(
Exception ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", "Une erreur interne est survenue");
body.put("type", "ErreurInterne");
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.jumeleordre.exception;
public class JumeleOrdreException extends RuntimeException {
public JumeleOrdreException(String message) {
super(message);
}
public JumeleOrdreException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,22 @@
// LimiteMiseDepasseeException.java
package com.pmumali.jumeleordre.exception;
public class LimiteMiseDepasseeException extends ParisInvalideException {
private final double miseActuelle;
private final double miseMaximum;
public LimiteMiseDepasseeException(double miseActuelle, double miseMaximum) {
super(String.format("La mise totale de %.2f FCFA dépasse la limite autorisée de %.2f FCFA",
miseActuelle, miseMaximum));
this.miseActuelle = miseActuelle;
this.miseMaximum = miseMaximum;
}
public double getMiseActuelle() {
return miseActuelle;
}
public double getMiseMaximum() {
return miseMaximum;
}
}

View File

@@ -0,0 +1,16 @@
// NombreChevauxInvalideException.java
package com.pmumali.jumeleordre.exception;
public class NombreChevauxInvalideException extends CourseInvalideException {
private final int nombreChevaux;
public NombreChevauxInvalideException(int nombreChevaux) {
super(String.format("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre (nombre actuel: %d)",
nombreChevaux));
this.nombreChevaux = nombreChevaux;
}
public int getNombreChevaux() {
return nombreChevaux;
}
}

View File

@@ -0,0 +1,12 @@
// PaiementInvalideException.java
package com.pmumali.jumeleordre.exception;
public class PaiementInvalideException extends JumeleOrdreException {
public PaiementInvalideException(String message) {
super(message);
}
public PaiementInvalideException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,12 @@
// ParisInvalideException.java
package com.pmumali.jumeleordre.exception;
public class ParisInvalideException extends JumeleOrdreException {
public ParisInvalideException(String message) {
super(message);
}
public ParisInvalideException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,12 @@
// ResultatCourseInvalideException.java
package com.pmumali.jumeleordre.exception;
public class ResultatCourseInvalideException extends JumeleOrdreException {
public ResultatCourseInvalideException(String message) {
super(message);
}
public ResultatCourseInvalideException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,17 @@
// ValidationException.java
package com.pmumali.jumeleordre.exception;
import java.util.List;
public class ValidationException extends JumeleOrdreException {
private final List<String> erreurs;
public ValidationException(String message, List<String> erreurs) {
super(message);
this.erreurs = erreurs;
}
public List<String> getErreurs() {
return erreurs;
}
}

View File

@@ -0,0 +1,17 @@
package com.pmumali.jumeleordre.model;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private boolean nonPartant;
@ManyToOne
private Course course;
}

View File

@@ -0,0 +1,23 @@
// Course.java
package com.pmumali.jumeleordre.model;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private LocalDateTime heureDebut;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<Cheval> chevaux;
private boolean estTerminee;
private boolean aDeadHeat;
}

View File

@@ -0,0 +1,34 @@
// Paris.java
package com.pmumali.jumeleordre.model;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Entity
@Data
public class Paris {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Course course;
@ManyToOne
private Cheval premier;
@ManyToOne
private Cheval deuxieme;
private double mise;
private LocalDateTime dateParis;
private String idParieur;
private StatutParis statut;
private Double gains;
public enum StatutParis {
EN_ATTENTE, GAGNANT, PERDANT, REMBOURSE
}
}

View File

@@ -0,0 +1,32 @@
// ResultatCourse.java
package com.pmumali.jumeleordre.model;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Entity
@Data
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
private Course course;
@ManyToOne
private Cheval premier;
@ManyToOne
private Cheval deuxieme;
@ElementCollection
private List<Long> chevauxDeadHeat;
private double totalMises;
private double masseAPartager;
private double prelevementsLegaux;
private double montantRembourse;
}

View File

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

View File

@@ -0,0 +1,11 @@
// CourseRepository.java
package com.pmumali.jumeleordre.repository;
import com.pmumali.jumeleordre.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByEstTermineeFalse();
}

View File

@@ -0,0 +1,14 @@
// ParisRepository.java
package com.pmumali.jumeleordre.repository;
import com.pmumali.jumeleordre.model.Paris;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ParisRepository extends JpaRepository<Paris, Long> {
List<Paris> findByCourseIdAndStatut(Long idCourse, Paris.StatutParis statut);
List<Paris> findByIdParieur(String idParieur);
List<Paris> findByCourseId(Long courseID);
}

View File

@@ -0,0 +1,9 @@
// ResultatCourseRepository.java
package com.pmumali.jumeleordre.repository;
import com.pmumali.jumeleordre.model.ResultatCourse;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
ResultatCourse findByCourseId(Long idCourse);
}

View File

@@ -0,0 +1,142 @@
package com.pmumali.jumeleordre.service;
import com.pmumali.jumeleordre.model.Course;
import com.pmumali.jumeleordre.model.Cheval;
import com.pmumali.jumeleordre.repository.CourseRepository;
import com.pmumali.jumeleordre.repository.ChevalRepository;
import com.pmumali.jumeleordre.exception.CourseInvalideException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
public class CourseService {
private final CourseRepository courseRepository;
private final ChevalRepository chevalRepository;
@Autowired
public CourseService(CourseRepository courseRepository, ChevalRepository chevalRepository) {
this.courseRepository = courseRepository;
this.chevalRepository = chevalRepository;
}
/**
* Récupère toutes les courses actives (non terminées)
*/
public List<Course> getCoursesActives() {
return courseRepository.findByEstTermineeFalse();
}
/**
* Crée une nouvelle course
*/
public Course creerCourse(String nomCourse, LocalDateTime heureDebut, List<Long> idsChevaux) throws CourseInvalideException {
if (idsChevaux.size() > 7) {
throw new CourseInvalideException("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre");
}
Course course = new Course();
course.setNom(nomCourse);
course.setHeureDebut(heureDebut);
course.setEstTerminee(false);
course.setADeadHeat(false);
Course savedCourse = courseRepository.save(course);
// Associer les chevaux à la course
List<Cheval> chevaux = chevalRepository.findAllById(idsChevaux);
for (Cheval cheval : chevaux) {
cheval.setCourse(savedCourse);
chevalRepository.save(cheval);
}
savedCourse.setChevaux(chevaux);
return courseRepository.save(savedCourse);
}
/**
* Récupère une course par son ID
*/
public Course getCourseById(Long id) throws CourseInvalideException {
return courseRepository.findById(id)
.orElseThrow(() -> new CourseInvalideException("Course non trouvée avec l'ID: " + id));
}
/**
* Met à jour les informations d'une course
*/
public Course mettreAJourCourse(Long id, String nom, LocalDateTime heureDebut) throws CourseInvalideException {
Course course = getCourseById(id);
if (nom != null) {
course.setNom(nom);
}
if (heureDebut != null) {
course.setHeureDebut(heureDebut);
}
return courseRepository.save(course);
}
/**
* Ajoute un cheval à une course
*/
public Course ajouterChevalACourse(Long idCourse, Long idCheval) throws CourseInvalideException {
Course course = getCourseById(idCourse);
Cheval cheval = chevalRepository.findById(idCheval)
.orElseThrow(() -> new CourseInvalideException("Cheval non trouvé avec l'ID: " + idCheval));
if (course.getChevaux().size() >= 7) {
throw new CourseInvalideException("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre");
}
cheval.setCourse(course);
chevalRepository.save(cheval);
course.getChevaux().add(cheval);
return courseRepository.save(course);
}
/**
* Marque un cheval comme non partant
*/
public Cheval declarerChevalNonPartant(Long idCheval) throws CourseInvalideException {
Cheval cheval = chevalRepository.findById(idCheval)
.orElseThrow(() -> new CourseInvalideException("Cheval non trouvé avec l'ID: " + idCheval));
cheval.setNonPartant(true);
return chevalRepository.save(cheval);
}
/**
* Supprime une course (si elle n'a pas encore commencé)
*/
public void supprimerCourse(Long id) throws CourseInvalideException {
Course course = getCourseById(id);
if (course.getHeureDebut().isBefore(LocalDateTime.now())) {
throw new CourseInvalideException("Impossible de supprimer une course qui a déjà commencé");
}
courseRepository.delete(course);
}
/**
* Récupère toutes les courses
*/
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
/**
* Vérifie si une course existe
*/
public boolean courseExiste(Long id) {
return courseRepository.existsById(id);
}
}

View File

@@ -0,0 +1,87 @@
// ParisService.java
package com.pmumali.jumeleordre.service;
import com.pmumali.jumeleordre.dto.ParisDto;
import com.pmumali.jumeleordre.exception.ParisInvalideException;
import com.pmumali.jumeleordre.model.Cheval;
import com.pmumali.jumeleordre.model.Course;
import com.pmumali.jumeleordre.model.Paris;
import com.pmumali.jumeleordre.repository.ChevalRepository;
import com.pmumali.jumeleordre.repository.CourseRepository;
import com.pmumali.jumeleordre.repository.ParisRepository;
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.List;
@Service
public class ParisService {
private static final double MISE_MINIMUM = 500;
private static final double MISE_MAXIMUM_PAR_PARIS = 20 * MISE_MINIMUM;
@Autowired
private ParisRepository parisRepository;
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
@Transactional
public Paris enregistrerParis(ParisDto parisDto) throws ParisInvalideException {
Course course = courseRepository.findById(parisDto.getIdCourse())
.orElseThrow(() -> new ParisInvalideException("Course non trouvée"));
if (course.isEstTerminee()) {
throw new ParisInvalideException("Impossible de parier sur une course terminée");
}
Cheval premier = chevalRepository.findById(parisDto.getIdChevalPremier())
.orElseThrow(() -> new ParisInvalideException("Cheval premier non trouvé"));
Cheval deuxieme = chevalRepository.findById(parisDto.getIdChevalDeuxieme())
.orElseThrow(() -> new ParisInvalideException("Cheval deuxième non trouvé"));
if (premier.equals(deuxieme)) {
throw new ParisInvalideException("Impossible de parier sur le même cheval pour les deux positions");
}
if (premier.isNonPartant() || deuxieme.isNonPartant()) {
throw new ParisInvalideException("Impossible de parier sur des chevaux non partants");
}
if (parisDto.getMise() < MISE_MINIMUM) {
throw new ParisInvalideException("La mise doit être d'au moins " + MISE_MINIMUM + " FCFA");
}
// Vérification du total des mises sur cette combinaison
double totalMisesCombinaison = parisRepository.findByCourseIdAndStatut(course.getId(), Paris.StatutParis.EN_ATTENTE)
.stream()
.filter(p -> p.getPremier().equals(premier) && p.getDeuxieme().equals(deuxieme))
.mapToDouble(Paris::getMise)
.sum();
if (totalMisesCombinaison + parisDto.getMise() > MISE_MAXIMUM_PAR_PARIS) {
throw new ParisInvalideException("Mise maximale pour cette combinaison dépassée");
}
Paris paris = new Paris();
paris.setCourse(course);
paris.setPremier(premier);
paris.setDeuxieme(deuxieme);
paris.setMise(parisDto.getMise());
paris.setDateParis(LocalDateTime.now());
paris.setIdParieur(parisDto.getIdParieur());
paris.setStatut(Paris.StatutParis.EN_ATTENTE);
return parisRepository.save(paris);
}
public List<Paris> getParisParParieur(String idParieur) {
return parisRepository.findByIdParieur(idParieur);
}
}

View File

@@ -0,0 +1,154 @@
// ResultatCourseService.java
package com.pmumali.jumeleordre.service;
import com.pmumali.jumeleordre.dto.ResultatCourseDto;
import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException;
import com.pmumali.jumeleordre.model.*;
import com.pmumali.jumeleordre.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ResultatCourseService {
@Autowired
private CourseRepository courseRepository;
@Autowired
private ChevalRepository chevalRepository;
@Autowired
private ParisRepository parisRepository;
@Autowired
private ResultatCourseRepository resultatCourseRepository;
@Transactional
public void traiterResultatCourse(ResultatCourseDto resultatDto) throws ResultatCourseInvalideException {
Course course = courseRepository.findById(resultatDto.getIdCourse())
.orElseThrow(() -> new ResultatCourseInvalideException("Course non trouvée"));
if (course.isEstTerminee()) {
throw new ResultatCourseInvalideException("Résultats déjà traités pour cette course");
}
Cheval premier = chevalRepository.findById(resultatDto.getIdChevalPremier())
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval premier non trouvé"));
Cheval deuxieme = chevalRepository.findById(resultatDto.getIdChevalDeuxieme())
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval deuxième non trouvé"));
// Validation des chevaux en dead heat
if (resultatDto.getChevauxDeadHeat() != null) {
for (Long idCheval : resultatDto.getChevauxDeadHeat()) {
chevalRepository.findById(idCheval)
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval en dead heat non trouvé"));
}
}
// Enregistrement du résultat
ResultatCourse resultat = new ResultatCourse();
resultat.setCourse(course);
resultat.setPremier(premier);
resultat.setDeuxieme(deuxieme);
resultat.setChevauxDeadHeat(resultatDto.getChevauxDeadHeat());
// Calcul des totaux et déductions
List<Paris> tousParis = parisRepository.findByCourseId(course.getId());
double totalMises = tousParis.stream().mapToDouble(Paris::getMise).sum();
double prelevementsLegaux = calculerPrelevementsLegaux(totalMises);
double montantRembourse = calculerMontantRembourse(tousParis);
double masseAPartager = totalMises - prelevementsLegaux - montantRembourse;
resultat.setTotalMises(totalMises);
resultat.setPrelevementsLegaux(prelevementsLegaux);
resultat.setMontantRembourse(montantRembourse);
resultat.setMasseAPartager(masseAPartager);
resultatCourseRepository.save(resultat);
// Traitement des paris
traiterParis(tousParis, resultat);
// Marquer la course comme terminée
course.setEstTerminee(true);
course.setADeadHeat(resultatDto.getChevauxDeadHeat() != null && !resultatDto.getChevauxDeadHeat().isEmpty());
courseRepository.save(course);
}
private double calculerPrelevementsLegaux(double totalMises) {
// Implémentation selon les exigences légales
return totalMises * 0.10; // Exemple: 10% de prélèvement
}
private double calculerMontantRembourse(List<Paris> paris) {
return paris.stream()
.filter(p -> p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant())
.mapToDouble(Paris::getMise)
.sum();
}
private void traiterParis(List<Paris> paris, ResultatCourse resultat) {
boolean aDeadHeat = resultat.getChevauxDeadHeat() != null && !resultat.getChevauxDeadHeat().isEmpty();
for (Paris p : paris) {
// Vérification des non-partants (Article 4)
if (p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant()) {
p.setStatut(Paris.StatutParis.REMBOURSE);
p.setGains(p.getMise());
parisRepository.save(p);
continue;
}
// Vérification des paris gagnants
if (estParisGagnant(p, resultat, aDeadHeat)) {
p.setStatut(Paris.StatutParis.GAGNANT);
double gains = calculerGains(p, resultat);
p.setGains(gains);
} else {
p.setStatut(Paris.StatutParis.PERDANT);
p.setGains(0.0);
}
parisRepository.save(p);
}
}
private boolean estParisGagnant(Paris paris, ResultatCourse resultat, boolean aDeadHeat) {
if (aDeadHeat) {
return estParisGagnantAvecDeadHeat(paris, resultat);
} else {
// Cas normal (Article 1)
return paris.getPremier().equals(resultat.getPremier()) &&
paris.getDeuxieme().equals(resultat.getDeuxieme());
}
}
private boolean estParisGagnantAvecDeadHeat(Paris paris, ResultatCourse resultat) {
// Implémentation pour les cas de dead heat (Article 3)
List<Long> chevauxDeadHeat = resultat.getChevauxDeadHeat();
if (chevauxDeadHeat.contains(paris.getPremier().getId())) {
// Dead heat à la première place
return chevauxDeadHeat.contains(paris.getDeuxieme().getId()) ||
paris.getDeuxieme().equals(resultat.getDeuxieme());
} else if (chevauxDeadHeat.contains(paris.getDeuxieme().getId())) {
// Dead heat à la deuxième place
return paris.getPremier().equals(resultat.getPremier());
}
return false;
}
private double calculerGains(Paris paris, ResultatCourse resultat) {
// Calcul simplifié - implémenter la logique complète basée sur les Articles 5, 8, 9
double gainsBase = paris.getMise() * 5.0; // Exemple de ratio de gains
return Math.max(gainsBase, paris.getMise() * 1.1); // Minimum 1.1 par unité (Article 5d)
}
}

View File

@@ -0,0 +1,97 @@
package com.pmumali.quarteplus.controller;
import com.pmumali.quarteplus.model.*;
import com.pmumali.quarteplus.repository.PaiementRepository;
import com.pmumali.quarteplus.repository.PariQuartePlusRepository;
import com.pmumali.quarteplus.service.ServiceQuartePlus;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/quarte-plus")
@RequiredArgsConstructor
public class ControleurQuartePlus {
private final ServiceQuartePlus service;
private final PariQuartePlusRepository pariRepository;
private final PaiementRepository paiementRepository;
@PostMapping("/pari")
public ResponseEntity<PariQuartePlus> placerPari(@RequestBody RequetePari requete) {
try {
PariQuartePlus pari = service.placerPari(requete);
return ResponseEntity.ok(pari);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/calculer-paiements")
public ResponseEntity<List<ReponsePaiement>> calculerPaiements(@RequestBody RequeteResultat requete) {
try {
List<ReponsePaiement> paiements = service.calculerPaiements(requete);
return ResponseEntity.ok(paiements);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/dead-heat/{type}")
public ResponseEntity<List<ReponsePaiement>> gererDeadHeat(
@RequestBody RequeteResultat requete,
@PathVariable TypeDeadHeat type) {
try {
List<ReponsePaiement> paiements = service.gererDeadHeat(requete, type);
return ResponseEntity.ok(paiements);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/calcul-combinaison")
public ResponseEntity<CalculCombinaison> calculerCombinaison(
@RequestParam TypePari typePari,
@RequestParam Integer nombreChevaux) {
try {
CalculCombinaison resultat = service.calculerCombinaison(typePari, nombreChevaux);
return ResponseEntity.ok(resultat);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/cagnotte")
public ResponseEntity<Cagnotte> creerCagnotte(@RequestParam Double montant) {
try {
Cagnotte cagnotte = service.creerCagnotte(montant);
return ResponseEntity.ok(cagnotte);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/cagnotte/{cagnotteId}/utiliser")
public ResponseEntity<Void> utiliserCagnotte(
@PathVariable Long cagnotteId,
@RequestParam Long courseId) {
try {
service.utiliserCagnotte(cagnotteId, courseId);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/paris/course/{courseId}")
public ResponseEntity<List<PariQuartePlus>> getParisCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(pariRepository.findByCourseId(courseId));
}
@GetMapping("/paiements/pari/{pariId}")
public ResponseEntity<List<Paiement>> getPaiementsPari(@PathVariable Long pariId) {
return ResponseEntity.ok(paiementRepository.findByPariId(pariId));
}
}

View File

@@ -0,0 +1,24 @@
package com.pmumali.quarteplus.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GestionnaireExceptions {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
return ResponseEntity.internalServerError().body("Erreur interne: " + ex.getMessage());
}
}

View File

@@ -0,0 +1,23 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "cagnotte")
public class Cagnotte {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Double montant;
private LocalDateTime dateCreation;
private LocalDateTime dateUtilisation;
private Boolean utilisee;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.quarteplus.model;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class CalculCombinaison {
private TypePari typePari;
private Integer nombreChevaux;
private Integer nombreCombinaisons;
private Double valeurMise;
}

View File

@@ -0,0 +1,22 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "cheval")
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private Integer numero;
private Boolean nonPartant;
}

View File

@@ -0,0 +1,28 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private LocalDateTime heureCourse;
@OneToMany
private List<Cheval> chevaux;
@Enumerated(EnumType.STRING)
private StatutCourse statut;
}

View File

@@ -0,0 +1,27 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "paiement")
public class Paiement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private PariQuartePlus pari;
private Double montant;
private LocalDateTime heurePaiement;
@Enumerated(EnumType.STRING)
private TypePaiement typePaiement;
}

View File

@@ -0,0 +1,37 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "pari_quarte_plus")
public class PariQuartePlus {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Course course;
@OrderColumn(name = "ordre_cheval")
@ManyToMany
private List<Cheval> chevauxSelectionnes;
private Double mise;
private LocalDateTime heurePari;
@Enumerated(EnumType.STRING)
private TypePari typePari;
@ManyToOne
private Parieur parieur;
private Boolean validationOrdreExact;
}

View File

@@ -0,0 +1,20 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "parieur")
public class Parieur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private String identification;
private Double miseTotale;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.quarteplus.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ReponsePaiement {
private Long pariId;
private Double montant;
private TypePaiement typePaiement;
private String message;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.quarteplus.model;
import lombok.Data;
import java.util.List;
@Data
public class RequetePari {
private Long courseId;
private List<Long> chevalIds;
private Double mise;
private TypePari typePari;
private Long parieurId;
}

View File

@@ -0,0 +1,17 @@
package com.pmumali.quarteplus.model;
import lombok.Data;
import java.util.List;
@Data
public class RequeteResultat {
private Long courseId;
private List<Long> ordreArriveeIds;
private List<Long> premiersIds;
private List<Long> secondsIds;
private List<Long> troisiemesIds;
private List<Long> quatriemesIds;
private Double recetteNette;
private Double prelevementsLegaux;
}

View File

@@ -0,0 +1,42 @@
package com.pmumali.quarteplus.model;
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "resultat_course")
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Course course;
@OrderColumn(name = "ordre_arrivee")
@ManyToMany
private List<Cheval> ordreArrivee;
@ManyToMany
private List<Cheval> premiers;
@ManyToMany
private List<Cheval> seconds;
@ManyToMany
private List<Cheval> troisiemes;
@ManyToMany
private List<Cheval> quatriemes;
private Double recetteNette;
private Double montantRembourse;
private Double prelevementsLegaux;
private Double masseAPartager;
}

View File

@@ -0,0 +1,5 @@
package com.pmumali.quarteplus.model;
public enum StatutCourse {
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
}

View File

@@ -0,0 +1,12 @@
package com.pmumali.quarteplus.model;
public enum TypeDeadHeat {
QUATRE_PREMIERS_OU_PLUS,
TROIS_PREMIERS_UN_QUATRIEME,
DEUX_PREMIERS_DEUX_TROISIEMES,
DEUX_PREMIERS_UN_TROISIEME_UN_QUATRIEME,
TROIS_SECONDS_OU_PLUS,
DEUX_SECONDS_UN_QUATRIEME,
DEUX_TROISIEMES_OU_PLUS,
DEUX_QUATRIEMES_OU_PLUS
}

View File

@@ -0,0 +1,9 @@
package com.pmumali.quarteplus.model;
public enum TypePaiement {
QUARTE_PLUS_ORDRE_EXACT,
QUARTE_PLUS_ORDRE_INEXACT,
BONUS_3,
BONUS_3_BIS,
REMBOURSEMENT
}

View File

@@ -0,0 +1,6 @@
package com.pmumali.quarteplus.model;
public enum TypePari {
UNITAIRE, COMBINE, CHAMP_TOTAL_3, CHAMP_PARTIEL_3,
CHAMP_TOTAL_2, CHAMP_PARTIEL_2, CHAMP_TOTAL_1, CHAMP_PARTIEL_1
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.Cagnotte;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CagnotteRepository extends JpaRepository<Cagnotte, Long> {
List<Cagnotte> findByUtilisee(Boolean utilisee);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.Cheval;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
List<Cheval> findByNonPartant(Boolean nonPartant);
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.Course;
import com.pmumali.quarteplus.model.StatutCourse;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByStatut(StatutCourse statut);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.Paiement;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PaiementRepository extends JpaRepository<Paiement, Long> {
List<Paiement> findByPariId(Long pariId);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.PariQuartePlus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PariQuartePlusRepository extends JpaRepository<PariQuartePlus, Long> {
List<PariQuartePlus> findByCourseId(Long courseId);
List<PariQuartePlus> findByParieurId(Long parieurId);
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.quarteplus.repository;
import com.pmumali.quarteplus.model.Parieur;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ParieurRepository extends JpaRepository<Parieur, Long> {
List<Parieur> findByNomContaining(String nom);
}

View File

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

View File

@@ -0,0 +1,563 @@
package com.pmumali.quarteplus.service;
import com.pmumali.quarteplus.model.*;
import com.pmumali.quarteplus.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ServiceQuartePlus {
private final PariQuartePlusRepository pariRepository;
private final CourseRepository courseRepository;
private final ChevalRepository chevalRepository;
private final PaiementRepository paiementRepository;
private final ResultatCourseRepository resultatRepository;
private final CagnotteRepository cagnotteRepository;
private final ParieurRepository parieurRepository;
private static final Double MISE_BASE = 500.0;
private static final Double MISE_MAX = 20 * MISE_BASE;
@Transactional
public PariQuartePlus placerPari(RequetePari requete) {
// Validation de la mise
if (requete.getMise() < MISE_BASE) {
throw new IllegalArgumentException("La mise doit être au moins " + MISE_BASE + " FCFA");
}
Course course = courseRepository.findById(requete.getCourseId())
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
List<Cheval> chevaux = chevalRepository.findAllById(requete.getChevalIds());
if (chevaux.size() != 4) {
throw new IllegalArgumentException("Exactement 4 chevaux doivent être sélectionnés");
}
// Vérification des non-partants
long nonPartants = chevaux.stream().filter(Cheval::getNonPartant).count();
if (nonPartants >= 2) {
throw new IllegalArgumentException("Maximum 1 cheval non-partant autorisé");
}
// Limitation de mise selon l'article 2
Double miseEffective = Math.min(requete.getMise(), MISE_MAX);
Parieur parieur = parieurRepository.findById(requete.getParieurId()).orElseThrow(() -> new RuntimeException("Parieur non trouvé"));
PariQuartePlus pari = PariQuartePlus.builder()
.course(course)
.chevauxSelectionnes(chevaux)
.mise(miseEffective)
.heurePari(LocalDateTime.now())
.typePari(requete.getTypePari())
.parieur(parieur)
.validationOrdreExact(false)
.build();
return pariRepository.save(pari);
}
@Transactional
public List<ReponsePaiement> calculerPaiements(RequeteResultat requete) {
ResultatCourse resultat = creerResultat(requete);
List<PariQuartePlus> paris = pariRepository.findByCourseId(requete.getCourseId());
List<ReponsePaiement> paiements = new ArrayList<>();
// Calcul de la masse à partager selon l'article 5
Double masseAPartager = calculerMasseAPartager(resultat);
resultat.setMasseAPartager(masseAPartager);
resultatRepository.save(resultat);
// Compter les paris gagnants par type
Map<TypePaiement, Integer> compteurs = new HashMap<>();
Map<TypePaiement, Double> montantsTotaux = new HashMap<>();
for (PariQuartePlus pari : paris) {
ReponsePaiement paiement = calculerPaiementPari(pari, resultat, masseAPartager);
if (paiement != null) {
compteurs.put(paiement.getTypePaiement(),
compteurs.getOrDefault(paiement.getTypePaiement(), 0) + 1);
montantsTotaux.put(paiement.getTypePaiement(),
montantsTotaux.getOrDefault(paiement.getTypePaiement(), 0.0) + paiement.getMontant());
paiements.add(paiement);
enregistrerPaiement(pari, paiement);
}
}
// Ajuster les rapports selon les proportions minimales (Article 6)
ajusterProportionsMinimales(paiements, compteurs, montantsTotaux, masseAPartager);
return paiements;
}
private ReponsePaiement calculerPaiementPari(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
long nonPartants = chevauxPari.stream().filter(Cheval::getNonPartant).count();
// Article 4: Gestion des non-partants
if (nonPartants >= 2) {
return new ReponsePaiement(pari.getId(), pari.getMise(),
TypePaiement.REMBOURSEMENT, "Remboursement - 2+ non-partants");
}
// Vérifier l'ordre exact
boolean ordreExact = estOrdreExact(chevauxPari, resultat.getOrdreArrivee());
// Vérifier les positions
boolean tousEnTop4 = chevauxPari.stream().allMatch(cheval ->
estDansListe(cheval, resultat.getPremiers()) ||
estDansListe(cheval, resultat.getSeconds()) ||
estDansListe(cheval, resultat.getTroisiemes()) ||
estDansListe(cheval, resultat.getQuatriemes()));
if (ordreExact && tousEnTop4) {
Double montant = calculerMontantQuartePlusOrdreExact(masseAPartager);
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Quarte Plus ordre exact");
}
if (tousEnTop4) {
Double montant = calculerMontantQuartePlusOrdreInexact(masseAPartager);
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, "Quarte Plus ordre inexact");
}
// Vérifier Bonus 3 avec non-partant (Bonus 3 Bis)
if (nonPartants == 1 && estEligibleBonus3(pari, resultat)) {
Double montant = calculerMontantBonus3(masseAPartager) * 3; // Triple selon article 4b
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.BONUS_3_BIS, "Bonus 3 Bis avec non-partant");
}
// Vérifier Bonus 3 normal
if (estEligibleBonus3(pari, resultat)) {
Double montant = calculerMontantBonus3(masseAPartager);
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.BONUS_3, "Bonus 3");
}
return null;
}
private boolean estOrdreExact(List<Cheval> chevauxPari, List<Cheval> ordreArrivee) {
if (chevauxPari.size() != 4 || ordreArrivee.size() < 4) return false;
return chevauxPari.get(0).equals(ordreArrivee.get(0)) &&
chevauxPari.get(1).equals(ordreArrivee.get(1)) &&
chevauxPari.get(2).equals(ordreArrivee.get(2)) &&
chevauxPari.get(3).equals(ordreArrivee.get(3));
}
private boolean estDansListe(Cheval cheval, List<Cheval> liste) {
return liste != null && liste.contains(cheval);
}
private boolean estEligibleBonus3(PariQuartePlus pari, ResultatCourse resultat) {
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
if (chevauxPari.size() < 3 || resultat.getOrdreArrivee().size() < 3) {
return false;
}
// Vérifier si les 3 premiers du pari correspondent aux 3 premiers à l'arrivée
List<Cheval> troisPremiersArrivee = resultat.getOrdreArrivee().subList(0, 3);
List<Cheval> troisPremiersPari = chevauxPari.subList(0, 3);
if (!troisPremiersPari.equals(troisPremiersArrivee)) {
return false;
}
// Vérifier que le 4ème cheval n'est pas dans les 4 premiers (pour Bonus 3 normal)
Cheval quatriemePari = chevauxPari.get(3);
boolean estDansTop4 = estDansListe(quatriemePari, resultat.getPremiers()) ||
estDansListe(quatriemePari, resultat.getSeconds()) ||
estDansListe(quatriemePari, resultat.getTroisiemes()) ||
estDansListe(quatriemePari, resultat.getQuatriemes());
// Pour Bonus 3 normal, le 4ème ne doit pas être dans le top 4
// Pour Bonus 3 Bis (avec non-partant), on a déjà géré ça séparément
return !estDansTop4;
}
private Double calculerMasseAPartager(ResultatCourse resultat) {
// Article 5: MAP = RNET - MREMB - PRELEV
return resultat.getRecetteNette() -
resultat.getMontantRembourse() -
resultat.getPrelevementsLegaux();
}
private Double calculerMontantQuartePlusOrdreExact(Double masseAPartager) {
// Article 5: 40% de la masse pour l'ordre exact
return masseAPartager * 0.4;
}
private Double calculerMontantQuartePlusOrdreInexact(Double masseAPartager) {
// Article 5: 35% de la masse pour l'ordre inexact
return masseAPartager * 0.35;
}
private Double calculerMontantBonus3(Double masseAPartager) {
// Article 5: 25% de la masse pour le Bonus 3
return masseAPartager * 0.25;
}
private void enregistrerPaiement(PariQuartePlus pari, ReponsePaiement reponse) {
Paiement paiement = Paiement.builder()
.pari(pari)
.montant(reponse.getMontant())
.heurePaiement(LocalDateTime.now())
.typePaiement(reponse.getTypePaiement())
.build();
paiementRepository.save(paiement);
}
private ResultatCourse creerResultat(RequeteResultat requete) {
Course course = courseRepository.findById(requete.getCourseId())
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
return ResultatCourse.builder()
.course(course)
.ordreArrivee(chevalRepository.findAllById(requete.getOrdreArriveeIds()))
.premiers(chevalRepository.findAllById(requete.getPremiersIds()))
.seconds(chevalRepository.findAllById(requete.getSecondsIds()))
.troisiemes(chevalRepository.findAllById(requete.getTroisiemesIds()))
.quatriemes(chevalRepository.findAllById(requete.getQuatriemesIds()))
.recetteNette(requete.getRecetteNette())
.prelevementsLegaux(requete.getPrelevementsLegaux())
.montantRembourse(0.0)
.build();
}
private void ajusterProportionsMinimales(List<ReponsePaiement> paiements,
Map<TypePaiement, Integer> compteurs,
Map<TypePaiement, Double> montantsTotaux,
Double masseAPartager) {
// Article 6: Ajustement des proportions minimales
// Vérifier ratio ordre exact/inexact
Double montantOrdreExact = montantsTotaux.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_EXACT, 0.0);
Double montantOrdreInexact = montantsTotaux.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0.0);
if (montantOrdreExact > 0 && montantOrdreInexact > 0) {
double ratio = montantOrdreExact / montantOrdreInexact;
// Article 6a: Ratio minimal de 8 pour 1
if (ratio < 8.0) {
redistribuerMasse(paiements, compteurs, masseAPartager, 8.0);
}
}
// Vérifier ratio Bonus 3/Quarte inexact (Article 7)
Double montantBonus3 = montantsTotaux.getOrDefault(TypePaiement.BONUS_3, 0.0);
if (montantBonus3 > 0 && montantOrdreInexact > 0) {
double ratio = montantBonus3 / montantOrdreInexact;
// Article 7: Bonus 3 ne peut dépasser 1/4 du Quarte inexact
if (ratio > 0.25) {
ajusterRatioBonus3(paiements, compteurs, montantsTotaux, masseAPartager);
}
}
}
private void redistribuerMasse(List<ReponsePaiement> paiements,
Map<TypePaiement, Integer> compteurs,
Double masseAPartager, Double coefficient) {
// Implémentation de la redistribution selon l'article 6
int countOrdreExact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_EXACT, 0);
int countOrdreInexact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0);
if (countOrdreExact > 0 && countOrdreInexact > 0) {
double totalMisesPonderees = (countOrdreExact * coefficient) + countOrdreInexact;
double rapportBase = masseAPartager / totalMisesPonderees;
// Mettre à jour les montants
for (ReponsePaiement paiement : paiements) {
if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_EXACT) {
paiement.setMontant(rapportBase * coefficient);
} else if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_INEXACT) {
paiement.setMontant(rapportBase);
}
}
}
}
private void ajusterRatioBonus3(List<ReponsePaiement> paiements,
Map<TypePaiement, Integer> compteurs,
Map<TypePaiement, Double> montantsTotaux,
Double masseAPartager) {
// Article 7: Ajustement du ratio Bonus 3
int countOrdreInexact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0);
int countBonus3 = compteurs.getOrDefault(TypePaiement.BONUS_3, 0);
if (countOrdreInexact > 0 && countBonus3 > 0) {
double totalMisesPonderees = (countOrdreInexact * 4) + countBonus3;
double rapportBase = masseAPartager * 0.35 / totalMisesPonderees; // 35% pour l'ordre inexact
// Mettre à jour les montants
for (ReponsePaiement paiement : paiements) {
if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_INEXACT) {
paiement.setMontant(rapportBase * 4);
} else if (paiement.getTypePaiement() == TypePaiement.BONUS_3) {
paiement.setMontant(rapportBase);
}
}
}
}
// Méthodes pour les calculs de combinaisons (Article 8)
public CalculCombinaison calculerCombinaison(TypePari typePari, Integer nombreChevaux) {
Integer nombreCombinaisons = 0;
switch (typePari) {
case UNITAIRE:
nombreCombinaisons = 1;
break;
case COMBINE:
nombreCombinaisons = (nombreChevaux * (nombreChevaux - 1) *
(nombreChevaux - 2) * (nombreChevaux - 3)) / 24;
break;
case CHAMP_TOTAL_3:
nombreCombinaisons = 24 * (nombreChevaux - 3);
break;
case CHAMP_PARTIEL_3:
nombreCombinaisons = 24 * nombreChevaux;
break;
case CHAMP_TOTAL_2:
nombreCombinaisons = 12 * (nombreChevaux - 2) * (nombreChevaux - 3);
break;
case CHAMP_PARTIEL_2:
nombreCombinaisons = 12 * nombreChevaux * (nombreChevaux - 1);
break;
case CHAMP_TOTAL_1:
nombreCombinaisons = 4 * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3);
break;
case CHAMP_PARTIEL_1:
nombreCombinaisons = 4 * nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2);
break;
}
Double valeurMise = nombreCombinaisons * MISE_BASE;
return CalculCombinaison.builder()
.typePari(typePari)
.nombreChevaux(nombreChevaux)
.nombreCombinaisons(nombreCombinaisons)
.valeurMise(valeurMise)
.build();
}
// Méthodes pour gérer les dead-heat (Article 3)
@Transactional
public List<ReponsePaiement> gererDeadHeat(RequeteResultat requete, TypeDeadHeat typeDeadHeat) {
ResultatCourse resultat = creerResultat(requete);
List<PariQuartePlus> paris = pariRepository.findByCourseId(requete.getCourseId());
List<ReponsePaiement> paiements = new ArrayList<>();
Double masseAPartager = calculerMasseAPartager(resultat);
for (PariQuartePlus pari : paris) {
ReponsePaiement paiement = calculerPaiementDeadHeat(pari, resultat, masseAPartager, typeDeadHeat);
if (paiement != null) {
paiements.add(paiement);
enregistrerPaiement(pari, paiement);
}
}
return paiements;
}
private ReponsePaiement calculerPaiementDeadHeat(PariQuartePlus pari, ResultatCourse resultat,
Double masseAPartager, TypeDeadHeat typeDeadHeat) {
switch (typeDeadHeat) {
case QUATRE_PREMIERS_OU_PLUS:
return gererDeadHeatQuatrePremiers(pari, resultat, masseAPartager);
case TROIS_PREMIERS_UN_QUATRIEME:
return gererDeadHeatTroisPremiersUnQuatrieme(pari, resultat, masseAPartager);
case DEUX_PREMIERS_DEUX_TROISIEMES:
return gererDeadHeatDeuxPremiersDeuxTroisiemes(pari, resultat, masseAPartager);
case DEUX_PREMIERS_UN_TROISIEME_UN_QUATRIEME:
return gererDeadHeatDeuxPremiersUnTroisiemeUnQuatrieme(pari, resultat, masseAPartager);
case TROIS_SECONDS_OU_PLUS:
return gererDeadHeatTroisSeconds(pari, resultat, masseAPartager);
case DEUX_SECONDS_UN_QUATRIEME:
return gererDeadHeatDeuxSecondsUnQuatrieme(pari, resultat, masseAPartager);
case DEUX_TROISIEMES_OU_PLUS:
return gererDeadHeatDeuxTroisiemes(pari, resultat, masseAPartager);
case DEUX_QUATRIEMES_OU_PLUS:
return gererDeadHeatDeuxQuatriemes(pari, resultat, masseAPartager);
default:
return calculerPaiementPari(pari, resultat, masseAPartager);
}
}
private ReponsePaiement gererDeadHeatQuatrePremiers(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3a: Dead-heat de 4+ chevaux premiers
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
if (chevauxPari.stream().allMatch(cheval -> estDansListe(cheval, resultat.getPremiers()))) {
int nombreCombinaisons = compterCombinaisonsDeadHeat(resultat.getPremiers().size(), 4);
Double montant = masseAPartager / nombreCombinaisons;
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat 4+ premiers");
}
return null;
}
private ReponsePaiement gererDeadHeatTroisPremiersUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3b: Dead-heat de 3 premiers + 1+ quatrième
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
boolean troisPremiersOk = chevauxPari.subList(0, 3).stream()
.allMatch(cheval -> estDansListe(cheval, resultat.getPremiers()));
boolean quatriemeOk = estDansListe(chevauxPari.get(3), resultat.getQuatriemes());
if (troisPremiersOk && quatriemeOk) {
// Calcul spécifique avec 6 permutations ordre exact, 18 ordre inexact
Double montant = calculerMontantDeadHeat35(masseAPartager, true);
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat 3 premiers + 1 quatrième");
}
return null;
}
// Implémentations similaires pour les autres types de dead-heat...
private ReponsePaiement gererDeadHeatDeuxPremiersDeuxTroisiemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3c: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 2.0);
}
private ReponsePaiement gererDeadHeatDeuxPremiersUnTroisiemeUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3d: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
}
private ReponsePaiement gererDeadHeatTroisSeconds(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3e: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 2.0);
}
private ReponsePaiement gererDeadHeatDeuxSecondsUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3f: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
}
private ReponsePaiement gererDeadHeatDeuxTroisiemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3g: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
}
private ReponsePaiement gererDeadHeatDeuxQuatriemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
// Article 3h: Implémentation spécifique
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
}
private ReponsePaiement calculerPaiementGeneriqueDeadHeat(PariQuartePlus pari, ResultatCourse resultat,
Double masseAPartager, Double coefficient) {
// Méthode générique pour les dead-heat avec coefficient spécifique
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
if (chevauxPari.stream().allMatch(cheval ->
estDansListe(cheval, resultat.getPremiers()) ||
estDansListe(cheval, resultat.getSeconds()) ||
estDansListe(cheval, resultat.getTroisiemes()) ||
estDansListe(cheval, resultat.getQuatriemes()))) {
int nombreCombinaisons = compterCombinaisonsGagnantesDeadHeat(resultat);
Double montant = (masseAPartager * 0.4) / (nombreCombinaisons * coefficient);
return new ReponsePaiement(pari.getId(), montant,
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat générique");
}
return null;
}
private int compterCombinaisonsDeadHeat(int nombreChevaux, int prise) {
// Calcul des combinaisons C(n, k)
if (prise == 0) return 1;
if (nombreChevaux < prise) return 0;
int resultat = 1;
for (int i = 1; i <= prise; i++) {
resultat = resultat * (nombreChevaux - i + 1) / i;
}
return resultat;
}
private int compterCombinaisonsGagnantesDeadHeat(ResultatCourse resultat) {
// Comptage simplifié des combinaisons gagnantes
// Implémentation réelle dépendrait de la structure exacte du dead-heat
return 1;
}
private Double calculerMontantDeadHeat35(Double masseAPartager, boolean ordreExact) {
// Calcul spécifique pour dead-heat 3+1 avec ratio 6:18
double portion = ordreExact ? 0.4 : 0.35;
double ratio = ordreExact ? 6.0/24.0 : 18.0/24.0;
return masseAPartager * portion * ratio;
}
// Gestion de la cagnotte (Article 10)
@Transactional
public Cagnotte creerCagnotte(Double montant) {
Cagnotte cagnotte = Cagnotte.builder()
.montant(montant)
.dateCreation(LocalDateTime.now())
.utilisee(false)
.build();
return cagnotteRepository.save(cagnotte);
}
@Transactional
public void utiliserCagnotte(Long cagnotteId, Long courseId) {
Cagnotte cagnotte = cagnotteRepository.findById(cagnotteId)
.orElseThrow(() -> new RuntimeException("Cagnotte non trouvée"));
if (cagnotte.getUtilisee()) {
throw new RuntimeException("Cagnotte déjà utilisée");
}
cagnotte.setUtilisee(true);
cagnotte.setDateUtilisation(LocalDateTime.now());
cagnotteRepository.save(cagnotte);
// Ajouter le montant à la masse à partager de la course
ResultatCourse resultat = resultatRepository.findByCourseId(courseId);
if (resultat != null) {
resultat.setMasseAPartager(resultat.getMasseAPartager() + cagnotte.getMontant());
resultatRepository.save(resultat);
}
}
@Transactional
public List<Cagnotte> getCagnottesDisponibles() {
return cagnotteRepository.findByUtilisee(false);
}
// Méthodes utilitaires supplémentaires
public Map<TypePari, Integer> getStatistiquesParis(Long courseId) {
List<PariQuartePlus> paris = pariRepository.findByCourseId(courseId);
return paris.stream()
.collect(Collectors.groupingBy(PariQuartePlus::getTypePari,
Collectors.summingInt(p -> 1)));
}
public Double getMasseTotaleCourse(Long courseId) {
ResultatCourse resultat = resultatRepository.findByCourseId(courseId);
return resultat != null ? resultat.getMasseAPartager() : 0.0;
}
public List<Paiement> getHistoriquePaiementsParieur(Long parieurId) {
List<PariQuartePlus> paris = pariRepository.findByParieurId(parieurId);
return paris.stream()
.flatMap(pari -> paiementRepository.findByPariId(pari.getId()).stream())
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,76 @@
package com.pmumali.quatro.controller;
import com.pmumali.quatro.model.*;
import com.pmumali.quatro.repository.*;
import com.pmumali.quatro.service.ServiceQuatro;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/quatro")
@RequiredArgsConstructor
public class ControllerQuatro {
private final ServiceQuatro serviceQuatro;
private final PariRepository pariRepository;
private final PaiementRepository paiementRepository;
@PostMapping("/pari")
public ResponseEntity<Pari> placerPari(@RequestBody RequetePari requetePari) {
try {
Pari pari = serviceQuatro.placerPari(requetePari);
return ResponseEntity.ok(pari);
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}
@PostMapping("/calculer-paiements")
public ResponseEntity<List<ReponsePaiement>> calculerPaiements(@RequestBody RequeteResultatCourse requeteResultat) {
try {
List<ReponsePaiement> paiements = serviceQuatro.calculerPaiements(requeteResultat);
return ResponseEntity.ok(paiements);
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}
@PostMapping("/gerer-dead-heat")
public ResponseEntity<List<ReponsePaiement>> gererDeadHeat(@RequestBody RequeteResultatCourse requeteResultat) {
try {
List<ReponsePaiement> paiements = serviceQuatro.gererDeadHeat(requeteResultat);
return ResponseEntity.ok(paiements);
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}
@GetMapping("/paris/course/{courseId}")
public ResponseEntity<List<Pari>> getParisParCourse(@PathVariable Long courseId) {
return ResponseEntity.ok(pariRepository.findByCourseId(courseId));
}
@GetMapping("/paiements/pari/{pariId}")
public ResponseEntity<List<Paiement>> getPaiementsParPari(@PathVariable Long pariId) {
return ResponseEntity.ok(paiementRepository.findByPariId(pariId));
}
@GetMapping("/calcul-combinaisons")
public ResponseEntity<Integer> calculerCombinaisons(
@RequestParam TypePari typePari,
@RequestParam int nombreChevaux) {
int nombreCombinaisons = serviceQuatro.calculerNombreCombinaisons(typePari, nombreChevaux);
return ResponseEntity.ok(nombreCombinaisons);
}
@GetMapping("/calcul-valeur-mise")
public ResponseEntity<Double> calculerValeurMise(
@RequestParam TypePari typePari,
@RequestParam int nombreChevaux) {
double valeurMise = serviceQuatro.calculerValeurMise(typePari, nombreChevaux);
return ResponseEntity.ok(valeurMise);
}
}

View File

@@ -0,0 +1,24 @@
package com.pmumali.quatro.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GestionnaireExceptionsGlobal {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleExceptionGenerale(Exception ex) {
return ResponseEntity.internalServerError().body("Une erreur s'est produite: " + ex.getMessage());
}
}

View File

@@ -0,0 +1,29 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "cheval")
public class Cheval {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nom")
private String nom;
@Column(name = "numero")
private Integer numero;
@Column(name = "non_partant")
private boolean nonPartant;
}

View File

@@ -0,0 +1,35 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nom")
private String nom;
@Column(name = "heure_course")
private LocalDateTime heureCourse;
@OneToMany(fetch = FetchType.LAZY)
@JoinTable(name = "course_chevaux",
joinColumns = @JoinColumn(name = "course_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> chevaux;
@Enumerated(EnumType.STRING)
@Column(name = "statut")
private StatutCourse statut;
}

View File

@@ -0,0 +1,32 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "paiement")
public class Paiement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pari_id")
private Pari pari;
@Column(name = "montant")
private Double montant;
@Column(name = "heure_paiement")
private LocalDateTime heurePaiement;
@Enumerated(EnumType.STRING)
@Column(name = "type_paiement")
private TypePaiement typePaiement;
}

View File

@@ -0,0 +1,43 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "pari")
public class Pari {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
private Course course;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "pari_chevaux",
joinColumns = @JoinColumn(name = "pari_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> chevauxSelectionnes;
@Column(name = "mise")
private Double mise;
@Column(name = "heure_pari")
private LocalDateTime heurePari;
@Enumerated(EnumType.STRING)
@Column(name = "type_pari")
private TypePari typePari;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parieur_id")
private Parieur parieur;
}

View File

@@ -0,0 +1,25 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "parieur")
public class Parieur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nom")
private String nom;
@Column(name = "identification")
private String identification;
@Column(name = "mise_totale")
private Double miseTotale;
}

View File

@@ -0,0 +1,5 @@
package com.pmumali.quatro.model;
public enum Position {
PREMIER, SECOND, TROISIEME, QUATRIEME
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.quatro.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ReponsePaiement {
private Long pariId;
private Double montant;
private TypePaiement typePaiement;
private String message;
}

View File

@@ -0,0 +1,13 @@
package com.pmumali.quatro.model;
import lombok.Data;
import java.util.List;
@Data
public class RequetePari {
private Long courseId;
private List<Long> chevalIds;
private Double mise;
private TypePari typePari;
private Long parieurId;
}

View File

@@ -0,0 +1,16 @@
package com.pmumali.quatro.model;
import lombok.Data;
import java.util.List;
@Data
public class RequeteResultatCourse {
private Long courseId;
private List<Long> chevauxPremiers;
private List<Long> chevauxSeconds;
private List<Long> chevauxTroisiemes;
private List<Long> chevauxQuatriemes;
private Double recetteNette;
private Double prelevementsLegaux;
}

View File

@@ -0,0 +1,59 @@
package com.pmumali.quatro.model;
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "resultat_course")
public class ResultatCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
private Course course;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "resultat_premiers",
joinColumns = @JoinColumn(name = "resultat_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
@OrderColumn(name = "ordre_position")
private List<Cheval> premiers;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "resultat_seconds",
joinColumns = @JoinColumn(name = "resultat_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> seconds;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "resultat_troisiemes",
joinColumns = @JoinColumn(name = "resultat_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> troisiemes;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "resultat_quatriemes",
joinColumns = @JoinColumn(name = "resultat_id"),
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
private List<Cheval> quatriemes;
@Column(name = "masse_partager")
private Double massePartager;
@Column(name = "recette_nette")
private Double recetteNette;
@Column(name = "montant_rembourse")
private Double montantRembourse;
@Column(name = "prelevements_legaux")
private Double prelevementsLegaux;
}

View File

@@ -0,0 +1,5 @@
package com.pmumali.quatro.model;
public enum StatutCourse {
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
}

View File

@@ -0,0 +1,5 @@
package com.pmumali.quatro.model;
public enum TypePaiement {
QUATRO, SPECIAL_TRIO, SPECIAL_JUMELE, SPECIAL_GAGNANT, REMBOURSEMENT
}

View File

@@ -0,0 +1,6 @@
package com.pmumali.quatro.model;
public enum TypePari {
UNITAIRE, COMBINE, CHAMP_TOTAL_3, CHAMP_PARTIEL_3,
CHAMP_TOTAL_2, CHAMP_PARTIEL_2, CHAMP_TOTAL_1, CHAMP_PARTIEL_1
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.quatro.repository;
import com.pmumali.quatro.model.Cheval;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
List<Cheval> findByNonPartant(boolean nonPartant);
List<Cheval> findByIdIn(List<Long> ids);
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.quatro.repository;
import com.pmumali.quatro.model.Course;
import com.pmumali.quatro.model.StatutCourse;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByStatut(StatutCourse statut);
}

View File

@@ -0,0 +1,11 @@
package com.pmumali.quatro.repository;
import com.pmumali.quatro.model.Paiement;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PaiementRepository extends JpaRepository<Paiement, Long> {
List<Paiement> findByPariId(Long pariId);
List<Paiement> findByPariCourseId(Long courseId);
}

View File

@@ -0,0 +1,12 @@
package com.pmumali.quatro.repository;
import com.pmumali.quatro.model.Pari;
import com.pmumali.quatro.model.TypePari;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PariRepository extends JpaRepository<Pari, Long> {
List<Pari> findByCourseId(Long courseId);
List<Pari> findByParieurId(Long parieurId);
List<Pari> findByCourseIdAndTypePari(Long courseId, TypePari typePari);
}

View File

@@ -0,0 +1,10 @@
package com.pmumali.quatro.repository;
import com.pmumali.quatro.model.Parieur;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ParieurRepository extends JpaRepository<Parieur, Long> {
List<Parieur> findByNomContaining(String nom);
}

View File

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

View File

@@ -0,0 +1,283 @@
package com.pmumali.quatro.service;
import com.pmumali.quatro.model.*;
import com.pmumali.quatro.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ServiceQuatro {
private final PariRepository pariRepository;
private final CourseRepository courseRepository;
private final ChevalRepository chevalRepository;
private final PaiementRepository paiementRepository;
private final ParieurRepository parieurRepository;
private final ResultatCourseRepository resultatCourseRepository;
private static final double MISE_BASE = 500.0;
private static final double MISE_UNITAIRE_MAX = 20 * MISE_BASE;
@Transactional
public Pari placerPari(RequetePari requete) {
// Validation de la mise
if (requete.getMise() < MISE_BASE) {
throw new IllegalArgumentException("La mise doit être au moins " + MISE_BASE + " FCFA");
}
Course course = courseRepository.findById(requete.getCourseId())
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
List<Cheval> chevaux = chevalRepository.findAllById(requete.getChevalIds());
if (chevaux.size() != 4) {
throw new IllegalArgumentException("Exactement 4 chevaux doivent être sélectionnés");
}
// Vérification des chevaux non-partants
long nombreNonPartants = chevaux.stream().filter(Cheval::isNonPartant).count();
if (nombreNonPartants > 0) {
throw new IllegalArgumentException("Impossible de parier sur des chevaux non-partants");
}
// Limitation de la mise selon l'article 2
double miseEffective = Math.min(requete.getMise(), MISE_UNITAIRE_MAX);
Parieur parieur = parieurRepository.findById(requete.getParieurId())
.orElseThrow(() -> new RuntimeException("Parieur non trouvé"));
Pari pari = Pari.builder()
.course(course)
.chevauxSelectionnes(chevaux)
.mise(miseEffective)
.heurePari(LocalDateTime.now())
.typePari(requete.getTypePari())
.parieur(parieur)
.build();
return pariRepository.save(pari);
}
@Transactional
public List<ReponsePaiement> calculerPaiements(RequeteResultatCourse requeteResultat) {
ResultatCourse resultat = creerResultatCourse(requeteResultat);
List<Pari> tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId());
List<ReponsePaiement> paiements = new ArrayList<>();
for (Pari pari : tousLesParis) {
ReponsePaiement paiement = calculerPaiementPourPari(pari, resultat);
if (paiement != null) {
paiements.add(paiement);
creerEnregistrementPaiement(pari, paiement);
}
}
return paiements;
}
private ReponsePaiement calculerPaiementPourPari(Pari pari, ResultatCourse resultat) {
List<Cheval> chevauxSelectionnes = pari.getChevauxSelectionnes();
long nombreNonPartants = chevauxSelectionnes.stream().filter(Cheval::isNonPartant).count();
// Article 4: Cas des non-partants
if (nombreNonPartants >= 1) {
return gererCasNonPartants(pari, resultat, nombreNonPartants);
}
// Vérifier si tous les chevaux sélectionnés sont dans les 4 premières positions
boolean tousEnTop4 = chevauxSelectionnes.stream().allMatch(cheval ->
estChevalEnPosition(cheval, resultat.getPremiers()) ||
estChevalEnPosition(cheval, resultat.getSeconds()) ||
estChevalEnPosition(cheval, resultat.getTroisiemes()) ||
estChevalEnPosition(cheval, resultat.getQuatriemes()));
if (tousEnTop4) {
double part = calculerPartQuatro(resultat);
return new ReponsePaiement(pari.getId(), part, TypePaiement.QUATRO, "Paiement QUATRO");
}
return null;
}
private ReponsePaiement gererCasNonPartants(Pari pari, ResultatCourse resultat, long nombreNonPartants) {
List<Cheval> chevauxParticipants = pari.getChevauxSelectionnes().stream()
.filter(cheval -> !cheval.isNonPartant())
.toList();
switch ((int) nombreNonPartants) {
case 1:
if (chevauxParticipants.size() == 3 &&
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 3)) {
double partQuatro = calculerPartQuatro(resultat);
double partSpecialTrio = partQuatro / 4; // 1/4 du QUATRO
return new ReponsePaiement(pari.getId(), partSpecialTrio,
TypePaiement.SPECIAL_TRIO, "Paiement Spécial Trio");
}
break;
case 2:
if (chevauxParticipants.size() == 2 &&
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 2)) {
double partQuatro = calculerPartQuatro(resultat);
double partSpecialJumele = partQuatro / 8; // 1/8 du QUATRO
return new ReponsePaiement(pari.getId(), partSpecialJumele,
TypePaiement.SPECIAL_JUMELE, "Paiement Spécial Jumelé");
}
break;
case 3:
if (chevauxParticipants.size() == 1 &&
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 1)) {
double partQuatro = calculerPartQuatro(resultat);
double partSpecialGagnant = partQuatro / 16; // 1/16 du QUATRO
return new ReponsePaiement(pari.getId(), partSpecialGagnant,
TypePaiement.SPECIAL_GAGNANT, "Paiement Spécial Gagnant");
}
break;
case 4:
return new ReponsePaiement(pari.getId(), pari.getMise(),
TypePaiement.REMBOURSEMENT, "Remboursement complet - tous chevaux non-partants");
}
return null;
}
private boolean sontChevauxEnPositionsTop(List<Cheval> chevaux, ResultatCourse resultat, int positionTop) {
return chevaux.stream().allMatch(cheval ->
estChevalEnPosition(cheval, resultat.getPremiers()) ||
(positionTop >= 2 && estChevalEnPosition(cheval, resultat.getSeconds())) ||
(positionTop >= 3 && estChevalEnPosition(cheval, resultat.getTroisiemes())));
}
private boolean estChevalEnPosition(Cheval cheval, List<Cheval> chevauxPosition) {
return chevauxPosition != null && chevauxPosition.contains(cheval);
}
private double calculerPartQuatro(ResultatCourse resultat) {
double masseAPartager = resultat.getRecetteNette() - resultat.getMontantRembourse() - resultat.getPrelevementsLegaux();
// Calcul simplifié - l'implémentation réelle considérerait le nombre de paris gagnants
return masseAPartager; // Devrait être distribué entre les paris gagnants
}
private void creerEnregistrementPaiement(Pari pari, ReponsePaiement paiement) {
Paiement enregistrementPaiement = Paiement.builder()
.pari(pari)
.montant(paiement.getMontant())
.heurePaiement(LocalDateTime.now())
.typePaiement(paiement.getTypePaiement())
.build();
paiementRepository.save(enregistrementPaiement);
}
private ResultatCourse creerResultatCourse(RequeteResultatCourse requete) {
Course course = courseRepository.findById(requete.getCourseId())
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
ResultatCourse resultat = ResultatCourse.builder()
.course(course)
.premiers(chevalRepository.findAllById(requete.getChevauxPremiers()))
.seconds(chevalRepository.findAllById(requete.getChevauxSeconds()))
.troisiemes(chevalRepository.findAllById(requete.getChevauxTroisiemes()))
.quatriemes(chevalRepository.findAllById(requete.getChevauxQuatriemes()))
.recetteNette(requete.getRecetteNette())
.prelevementsLegaux(requete.getPrelevementsLegaux())
.montantRembourse(0.0) // Serait calculé basé sur les remboursements
.build();
return resultatCourseRepository.save(resultat);
}
// Méthodes pour gérer les dead-heat (Article 3)
@Transactional
public List<ReponsePaiement> gererDeadHeat(RequeteResultatCourse requeteResultat) {
ResultatCourse resultat = creerResultatCourse(requeteResultat);
List<Pari> tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId());
List<ReponsePaiement> paiements = new ArrayList<>();
// Implémentation des règles complexes de dead-heat
Map<String, List<Pari>> combinaisonsPayables = identifierCombinaisonsPayablesDeadHeat(resultat, tousLesParis);
for (Map.Entry<String, List<Pari>> entry : combinaisonsPayables.entrySet()) {
double part = calculerPartDeadHeat(resultat, entry.getValue());
for (Pari pari : entry.getValue()) {
ReponsePaiement paiement = new ReponsePaiement(pari.getId(), part, TypePaiement.QUATRO, "Paiement Dead-Heat");
paiements.add(paiement);
creerEnregistrementPaiement(pari, paiement);
}
}
return paiements;
}
private Map<String, List<Pari>> identifierCombinaisonsPayablesDeadHeat(ResultatCourse resultat, List<Pari> paris) {
Map<String, List<Pari>> combinaisons = new HashMap<>();
for (Pari pari : paris) {
String combinaison = genererCleCombinaison(pari.getChevauxSelectionnes());
if (estCombinaisonPayableDeadHeat(pari.getChevauxSelectionnes(), resultat)) {
combinaisons.computeIfAbsent(combinaison, k -> new ArrayList<>()).add(pari);
}
}
return combinaisons;
}
private boolean estCombinaisonPayableDeadHeat(List<Cheval> chevaux, ResultatCourse resultat) {
// Implémentation des règles spécifiques de dead-heat selon l'article 3
// Cette méthode devrait implémenter toutes les sous-règles a) à h)
return chevaux.stream().allMatch(cheval ->
estChevalEnPosition(cheval, resultat.getPremiers()) ||
estChevalEnPosition(cheval, resultat.getSeconds()) ||
estChevalEnPosition(cheval, resultat.getTroisiemes()) ||
estChevalEnPosition(cheval, resultat.getQuatriemes()));
}
private String genererCleCombinaison(List<Cheval> chevaux) {
return chevaux.stream()
.map(cheval -> String.valueOf(cheval.getId()))
.sorted()
.collect(Collectors.joining("-"));
}
private double calculerPartDeadHeat(ResultatCourse resultat, List<Pari> parisCombinaison) {
double masseAPartager = resultat.getMassePartager();
double totalMisesCombinaison = parisCombinaison.stream().mapToDouble(Pari::getMise).sum();
return (masseAPartager / parisCombinaison.size()) * (totalMisesCombinaison / masseAPartager);
}
// Méthodes pour les formules de pari (Article 6)
public int calculerNombreCombinaisons(TypePari typePari, int nombreChevaux) {
switch (typePari) {
case UNITAIRE:
return 1;
case COMBINE:
return (nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3)) / 24;
case CHAMP_TOTAL_3:
return 24 * (nombreChevaux - 3);
case CHAMP_PARTIEL_3:
return 24 * nombreChevaux;
case CHAMP_TOTAL_2:
return 12 * (nombreChevaux - 2) * (nombreChevaux - 3);
case CHAMP_PARTIEL_2:
return 12 * nombreChevaux * (nombreChevaux - 1);
case CHAMP_TOTAL_1:
return 4 * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3);
case CHAMP_PARTIEL_1:
return 4 * nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2);
default:
return 0;
}
}
public double calculerValeurMise(TypePari typePari, int nombreChevaux) {
int nombreCombinaisons = calculerNombreCombinaisons(typePari, nombreChevaux);
return nombreCombinaisons * MISE_BASE;
}
}

View File

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More