Initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/pmu/jumele/dto/PaiementResponse.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PaiementResponse.java
Normal 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;
|
||||
}
|
||||
14
src/main/java/com/pmu/jumele/dto/PariRequest.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PariRequest.java
Normal 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)
|
||||
}
|
||||
14
src/main/java/com/pmu/jumele/dto/PositionsRequest.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PositionsRequest.java
Normal 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
|
||||
}
|
||||
24
src/main/java/com/pmu/jumele/entity/PariEntity.java
Normal file
24
src/main/java/com/pmu/jumele/entity/PariEntity.java
Normal 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;
|
||||
}
|
||||
10
src/main/java/com/pmu/jumele/repository/PariRepository.java
Normal file
10
src/main/java/com/pmu/jumele/repository/PariRepository.java
Normal 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);
|
||||
}
|
||||
290
src/main/java/com/pmu/jumele/service/JumeleGagnantService.java
Normal file
290
src/main/java/com/pmu/jumele/service/JumeleGagnantService.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/pmu/jumele/util/CombinaisonUtil.java
Normal file
24
src/main/java/com/pmu/jumele/util/CombinaisonUtil.java
Normal 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(","));
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/pmu/jumele/util/FormulesTable.java
Normal file
35
src/main/java/com/pmu/jumele/util/FormulesTable.java
Normal 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));
|
||||
// ...
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java
Normal file
13
src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/main/java/com/pmu/mali/model/Pari.java
Normal file
16
src/main/java/com/pmu/mali/model/Pari.java
Normal 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)
|
||||
}
|
||||
16
src/main/java/com/pmu/mali/model/ResultatCourse.java
Normal file
16
src/main/java/com/pmu/mali/model/ResultatCourse.java
Normal 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)
|
||||
}
|
||||
6
src/main/java/com/pmu/mali/model/TypePari.java
Normal file
6
src/main/java/com/pmu/mali/model/TypePari.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package com.pmu.mali.model;
|
||||
|
||||
public enum TypePari {
|
||||
SIMPLE_GAGNANT,
|
||||
SIMPLE_PLACE
|
||||
}
|
||||
367
src/main/java/com/pmu/mali/service/CalculPariService.java
Normal file
367
src/main/java/com/pmu/mali/service/CalculPariService.java
Normal 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 d’abord 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
11
src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java
Normal file
11
src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java
Normal 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;
|
||||
}
|
||||
13
src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java
Normal file
13
src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/pmumali/jumeleordre/model/Cheval.java
Normal file
17
src/main/java/com/pmumali/jumeleordre/model/Cheval.java
Normal 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;
|
||||
}
|
||||
23
src/main/java/com/pmumali/jumeleordre/model/Course.java
Normal file
23
src/main/java/com/pmumali/jumeleordre/model/Course.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/pmumali/jumeleordre/model/Paris.java
Normal file
34
src/main/java/com/pmumali/jumeleordre/model/Paris.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
142
src/main/java/com/pmumali/jumeleordre/service/CourseService.java
Normal file
142
src/main/java/com/pmumali/jumeleordre/service/CourseService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/pmumali/quarteplus/model/Cagnotte.java
Normal file
23
src/main/java/com/pmumali/quarteplus/model/Cagnotte.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
22
src/main/java/com/pmumali/quarteplus/model/Cheval.java
Normal file
22
src/main/java/com/pmumali/quarteplus/model/Cheval.java
Normal 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;
|
||||
}
|
||||
28
src/main/java/com/pmumali/quarteplus/model/Course.java
Normal file
28
src/main/java/com/pmumali/quarteplus/model/Course.java
Normal 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;
|
||||
}
|
||||
27
src/main/java/com/pmumali/quarteplus/model/Paiement.java
Normal file
27
src/main/java/com/pmumali/quarteplus/model/Paiement.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
20
src/main/java/com/pmumali/quarteplus/model/Parieur.java
Normal file
20
src/main/java/com/pmumali/quarteplus/model/Parieur.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
13
src/main/java/com/pmumali/quarteplus/model/RequetePari.java
Normal file
13
src/main/java/com/pmumali/quarteplus/model/RequetePari.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.pmumali.quarteplus.model;
|
||||
|
||||
public enum StatutCourse {
|
||||
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
|
||||
}
|
||||
12
src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java
Normal file
12
src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
6
src/main/java/com/pmumali/quarteplus/model/TypePari.java
Normal file
6
src/main/java/com/pmumali/quarteplus/model/TypePari.java
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/pmumali/quatro/model/Cheval.java
Normal file
29
src/main/java/com/pmumali/quatro/model/Cheval.java
Normal 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;
|
||||
}
|
||||
35
src/main/java/com/pmumali/quatro/model/Course.java
Normal file
35
src/main/java/com/pmumali/quatro/model/Course.java
Normal 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;
|
||||
}
|
||||
32
src/main/java/com/pmumali/quatro/model/Paiement.java
Normal file
32
src/main/java/com/pmumali/quatro/model/Paiement.java
Normal 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;
|
||||
}
|
||||
43
src/main/java/com/pmumali/quatro/model/Pari.java
Normal file
43
src/main/java/com/pmumali/quatro/model/Pari.java
Normal 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;
|
||||
}
|
||||
25
src/main/java/com/pmumali/quatro/model/Parieur.java
Normal file
25
src/main/java/com/pmumali/quatro/model/Parieur.java
Normal 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;
|
||||
}
|
||||
5
src/main/java/com/pmumali/quatro/model/Position.java
Normal file
5
src/main/java/com/pmumali/quatro/model/Position.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.pmumali.quatro.model;
|
||||
|
||||
public enum Position {
|
||||
PREMIER, SECOND, TROISIEME, QUATRIEME
|
||||
}
|
||||
13
src/main/java/com/pmumali/quatro/model/ReponsePaiement.java
Normal file
13
src/main/java/com/pmumali/quatro/model/ReponsePaiement.java
Normal 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;
|
||||
}
|
||||
13
src/main/java/com/pmumali/quatro/model/RequetePari.java
Normal file
13
src/main/java/com/pmumali/quatro/model/RequetePari.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
59
src/main/java/com/pmumali/quatro/model/ResultatCourse.java
Normal file
59
src/main/java/com/pmumali/quatro/model/ResultatCourse.java
Normal 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;
|
||||
}
|
||||
5
src/main/java/com/pmumali/quatro/model/StatutCourse.java
Normal file
5
src/main/java/com/pmumali/quatro/model/StatutCourse.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.pmumali.quatro.model;
|
||||
|
||||
public enum StatutCourse {
|
||||
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
|
||||
}
|
||||
5
src/main/java/com/pmumali/quatro/model/TypePaiement.java
Normal file
5
src/main/java/com/pmumali/quatro/model/TypePaiement.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.pmumali.quatro.model;
|
||||
|
||||
public enum TypePaiement {
|
||||
QUATRO, SPECIAL_TRIO, SPECIAL_JUMELE, SPECIAL_GAGNANT, REMBOURSEMENT
|
||||
}
|
||||
6
src/main/java/com/pmumali/quatro/model/TypePari.java
Normal file
6
src/main/java/com/pmumali/quatro/model/TypePari.java
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
283
src/main/java/com/pmumali/quatro/service/ServiceQuatro.java
Normal file
283
src/main/java/com/pmumali/quatro/service/ServiceQuatro.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/pmumali/simple/dto/ChevalDto.java
Normal file
23
src/main/java/com/pmumali/simple/dto/ChevalDto.java
Normal 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;
|
||||
}
|
||||
}
|
||||
12
src/main/java/com/pmumali/simple/dto/CombinaisonDto.java
Normal file
12
src/main/java/com/pmumali/simple/dto/CombinaisonDto.java
Normal 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
|
||||
}
|
||||
15
src/main/java/com/pmumali/simple/dto/PariRequest.java
Normal file
15
src/main/java/com/pmumali/simple/dto/PariRequest.java
Normal 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
|
||||
}
|
||||
211
src/main/java/com/pmumali/simple/dto/PariResponse.java
Normal file
211
src/main/java/com/pmumali/simple/dto/PariResponse.java
Normal 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";
|
||||
}
|
||||
}
|
||||
16
src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java
Normal file
16
src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/pmumali/simple/model/Cheval.java
Normal file
68
src/main/java/com/pmumali/simple/model/Cheval.java
Normal 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;
|
||||
}
|
||||
}
|
||||
287
src/main/java/com/pmumali/simple/model/Client.java
Normal file
287
src/main/java/com/pmumali/simple/model/Client.java
Normal 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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/pmumali/simple/model/Combinaison.java
Normal file
35
src/main/java/com/pmumali/simple/model/Combinaison.java
Normal 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
Reference in New Issue
Block a user