Initial commit

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

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

38
build.gradle Normal file
View File

@@ -0,0 +1,38 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.pmu.mali'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'API-PLR'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
// CourseController.java
package com.pmumali.jumeleordre.controller;
import com.pmumali.jumeleordre.dto.ResultatCourseDto;
import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException;
import com.pmumali.jumeleordre.model.Course;
import com.pmumali.jumeleordre.service.ResultatCourseService;
import com.pmumali.jumeleordre.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@Autowired
private CourseService courseService;
@Autowired
private ResultatCourseService resultatCourseService;
@GetMapping("/actives")
public ResponseEntity<List<Course>> getCoursesActives() {
List<Course> courses = courseService.getCoursesActives();
return ResponseEntity.ok(courses);
}
@PostMapping("/resultat")
public ResponseEntity<Void> soumettreResultatCourse(@RequestBody ResultatCourseDto resultatDto) {
try {
resultatCourseService.traiterResultatCourse(resultatDto);
return ResponseEntity.ok().build();
} catch (ResultatCourseInvalideException e) {
return ResponseEntity.badRequest().build();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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