Initial commit
This commit is contained in:
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/gradlew text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.jar binary
|
||||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
38
build.gradle
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
251
gradlew
vendored
Executable 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
94
gradlew.bat
vendored
Normal 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
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'API-PLR'
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.pmu.jumele.controller;
|
||||||
|
|
||||||
|
import com.pmu.jumele.dto.*;
|
||||||
|
import com.pmu.jumele.service.JumeleGagnantService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/jumele")
|
||||||
|
public class JumeleController {
|
||||||
|
|
||||||
|
private final JumeleGagnantService service;
|
||||||
|
|
||||||
|
public JumeleController(JumeleGagnantService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/pari")
|
||||||
|
public ResponseEntity<?> enregistrerPari(@RequestBody PariRequest req) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> res = service.enregistrerPari(req);
|
||||||
|
return ResponseEntity.ok(res);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resultat")
|
||||||
|
public ResponseEntity<?> calculer(@RequestBody PositionsRequest req) {
|
||||||
|
Map<String, Object> res = service.calculer(req);
|
||||||
|
return ResponseEntity.ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/paris/{courseId}")
|
||||||
|
public ResponseEntity<?> listerParis(@PathVariable String courseId) {
|
||||||
|
return ResponseEntity.ok(service.getClass().getName()); // placeholder: add endpoint to fetch from repo if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/java/com/pmu/jumele/dto/PaiementResponse.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PaiementResponse.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.pmu.jumele.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PaiementResponse {
|
||||||
|
private String parieur;
|
||||||
|
private boolean gagnant;
|
||||||
|
private BigDecimal gain;
|
||||||
|
private String combinaison;
|
||||||
|
}
|
||||||
14
src/main/java/com/pmu/jumele/dto/PariRequest.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PariRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.pmu.jumele.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PariRequest {
|
||||||
|
private String parieur;
|
||||||
|
private String courseId;
|
||||||
|
private List<Integer> chevaux; // can be 2 (unit), or list to generate combinations (combiné/champ)
|
||||||
|
private int mise; // montant total for the formula (we'll split for unitary bets)
|
||||||
|
private String formuleType; // "unitaire","combine","champ_total","champ_partiel"
|
||||||
|
private List<Integer> champSelection; // used for champ partiel (if formuleType == champ_partiel)
|
||||||
|
}
|
||||||
14
src/main/java/com/pmu/jumele/dto/PositionsRequest.java
Normal file
14
src/main/java/com/pmu/jumele/dto/PositionsRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.pmu.jumele.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PositionsRequest {
|
||||||
|
private String courseId;
|
||||||
|
// positions: list of positions; each position is a list of horse numbers (to represent dead-heat)
|
||||||
|
// positions.get(0) = list of horses classified first
|
||||||
|
// positions.get(1) = list of horses classified second, etc.
|
||||||
|
private List<List<Integer>> positions;
|
||||||
|
private List<Integer> nonPartants; // optional
|
||||||
|
}
|
||||||
24
src/main/java/com/pmu/jumele/entity/PariEntity.java
Normal file
24
src/main/java/com/pmu/jumele/entity/PariEntity.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.pmu.jumele.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "paris_jumele")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class PariEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String parieur;
|
||||||
|
private String courseId;
|
||||||
|
// store combinaison as "a,b"
|
||||||
|
private String combinaison;
|
||||||
|
private BigDecimal mise;
|
||||||
|
}
|
||||||
10
src/main/java/com/pmu/jumele/repository/PariRepository.java
Normal file
10
src/main/java/com/pmu/jumele/repository/PariRepository.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmu.jumele.repository;
|
||||||
|
|
||||||
|
import com.pmu.jumele.entity.PariEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PariRepository extends JpaRepository<PariEntity, Long> {
|
||||||
|
List<PariEntity> findByCourseId(String courseId);
|
||||||
|
List<PariEntity> findByCourseIdAndParieurAndCombinaison(String courseId, String parieur, String combinaison);
|
||||||
|
}
|
||||||
290
src/main/java/com/pmu/jumele/service/JumeleGagnantService.java
Normal file
290
src/main/java/com/pmu/jumele/service/JumeleGagnantService.java
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package com.pmu.jumele.service;
|
||||||
|
|
||||||
|
import com.pmu.jumele.dto.*;
|
||||||
|
import com.pmu.jumele.entity.PariEntity;
|
||||||
|
import com.pmu.jumele.repository.PariRepository;
|
||||||
|
import com.pmu.jumele.util.CombinaisonUtil;
|
||||||
|
import com.pmu.jumele.util.FormulesTable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class JumeleGagnantService {
|
||||||
|
|
||||||
|
private final PariRepository pariRepository;
|
||||||
|
private static final BigDecimal MISE_MIN = BigDecimal.valueOf(500);
|
||||||
|
private static final int PLAFOND_MULT = 20;
|
||||||
|
private static final BigDecimal RAPPORT_MIN = BigDecimal.valueOf(1.1);
|
||||||
|
private static final int SCALE = 8;
|
||||||
|
|
||||||
|
// prélèvements légaux (paramétrable)
|
||||||
|
private BigDecimal prelevements = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
public JumeleGagnantService(PariRepository pariRepository) {
|
||||||
|
this.pariRepository = pariRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrelevements(BigDecimal p) { this.prelevements = p == null ? BigDecimal.ZERO : p; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un pari request — peut être unitaire ou une formule.
|
||||||
|
* Pour formules, convertit en paris unitaires (combinaisons) et enregistre chaque unité en respectant le tableau de valeurs.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> enregistrerPari(PariRequest req) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// validation minimale
|
||||||
|
if (req.getMise() < MISE_MIN.intValue()) throw new IllegalArgumentException("Mise minimum = 500");
|
||||||
|
|
||||||
|
// calculer unités
|
||||||
|
Set<Set<Integer>> combsToPlace = new HashSet<>();
|
||||||
|
if ("unitaire".equalsIgnoreCase(req.getFormuleType()) || req.getChevaux().size() == 2) {
|
||||||
|
if (req.getChevaux().size() != 2) throw new IllegalArgumentException("Pari unitaire doit contenir 2 chevaux");
|
||||||
|
combsToPlace.add(new HashSet<>(req.getChevaux()));
|
||||||
|
} else if ("combine".equalsIgnoreCase(req.getFormuleType())) {
|
||||||
|
combsToPlace = CombinaisonUtil.allPairs(req.getChevaux());
|
||||||
|
} else if ("champ_total".equalsIgnoreCase(req.getFormuleType())) {
|
||||||
|
// champ total: base x all others; req.getChevaux() must contain base + others?
|
||||||
|
// For API simplicity: req.chevaux contains full list of partants; champ_total of a base is handled client-side by sending combos
|
||||||
|
combsToPlace = CombinaisonUtil.allPairs(req.getChevaux());
|
||||||
|
} else if ("champ_partiel".equalsIgnoreCase(req.getFormuleType())) {
|
||||||
|
if (req.getChampSelection() == null || req.getChampSelection().isEmpty())
|
||||||
|
throw new IllegalArgumentException("Champ partiel nécessite champSelection");
|
||||||
|
// base is first element of req.chevaux
|
||||||
|
Integer base = req.getChevaux().get(0);
|
||||||
|
for (Integer c : req.getChampSelection()) {
|
||||||
|
combsToPlace.add(new HashSet<>(Arrays.asList(base, c)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("FormuleType inconnu");
|
||||||
|
}
|
||||||
|
|
||||||
|
// valeur unitaire par combinaison : on divise la mise totale proportionnellement selon tableau ou uniformément
|
||||||
|
// Simplicité : on divise la mise totale uniformément par le nombre de combinaisons
|
||||||
|
int nbComb = combsToPlace.size();
|
||||||
|
BigDecimal totalMise = BigDecimal.valueOf(req.getMise());
|
||||||
|
BigDecimal miseUnitaire = totalMise.divide(BigDecimal.valueOf(nbComb), SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// enregistrement avec respect du plafond 20 prises par parieur sur même combinaison
|
||||||
|
BigDecimal plafond = MISE_MIN.multiply(BigDecimal.valueOf(PLAFOND_MULT));
|
||||||
|
BigDecimal totalRembourse = BigDecimal.ZERO;
|
||||||
|
List<PariEntity> enregistrés = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Set<Integer> comb : combsToPlace) {
|
||||||
|
String combStr = CombinaisonUtil.combToString(comb);
|
||||||
|
// somme déjà engagée par ce parieur sur cette combinaison dans la même course
|
||||||
|
List<PariEntity> deja = pariRepository.findByCourseIdAndParieurAndCombinaison(req.getCourseId(), req.getParieur(), combStr);
|
||||||
|
BigDecimal dejaTotal = deja.stream().map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
BigDecimal disponible = plafond.subtract(dejaTotal);
|
||||||
|
BigDecimal toRecord = miseUnitaire.min(disponible);
|
||||||
|
BigDecimal toRefund = miseUnitaire.subtract(toRecord);
|
||||||
|
if (toRecord.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
PariEntity e = PariEntity.builder()
|
||||||
|
.parieur(req.getParieur())
|
||||||
|
.courseId(req.getCourseId())
|
||||||
|
.combinaison(combStr)
|
||||||
|
.mise(toRecord)
|
||||||
|
.build();
|
||||||
|
pariRepository.save(e);
|
||||||
|
enregistrés.add(e);
|
||||||
|
}
|
||||||
|
if (toRefund.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
totalRembourse = totalRembourse.add(toRefund);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("enregistres", enregistrés.size());
|
||||||
|
result.put("a_rembourser", totalRembourse.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre le résultat via PositionsRequest (positions = list of lists to express dead-heats).
|
||||||
|
*/
|
||||||
|
public void enregistrerResultat(PositionsRequest req) {
|
||||||
|
// store positions in memory — for calculation we require positions per course; for simplicity, pass positions during calcul.
|
||||||
|
// In this implementation, we'll pass PositionsRequest directly to calculer.
|
||||||
|
// To persist results, create a ResultEntity (omitted here).
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculer paiements pour une course en fournissant PositionsRequest (dead-heat possible).
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Map<String, Object> calculer(PositionsRequest positionsReq) {
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
String courseId = positionsReq.getCourseId();
|
||||||
|
List<PariEntity> parisCourse = pariRepository.findByCourseId(courseId);
|
||||||
|
List<Integer> nonPartants = positionsReq.getNonPartants() == null ? Collections.emptyList() : positionsReq.getNonPartants();
|
||||||
|
|
||||||
|
// 1) Générer combinaisons payables selon article 3:
|
||||||
|
// positionsReq.positions[0] => premiers (list), positionsReq.positions[1] => deuxiemes (list), etc.
|
||||||
|
List<List<Integer>> positions = positionsReq.getPositions();
|
||||||
|
if (positions == null || positions.size() < 2) {
|
||||||
|
// moins de deux classés => tout remboursé (Article 8)
|
||||||
|
List<PaiementResponse> remboursements = parisCourse.stream()
|
||||||
|
.map(p -> new PaiementResponse(p.getParieur(), false, p.getMise(), p.getCombinaison()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
out.put("paiements", remboursements);
|
||||||
|
out.put("cagnotte", BigDecimal.ZERO.setScale(2));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Set<Integer>> combPayables = new HashSet<>();
|
||||||
|
// a) dead-heat premiers (positions[0] size >=2) -> toutes combinaisons 2-à-2 parmi premiers
|
||||||
|
List<Integer> premiers = positions.get(0);
|
||||||
|
if (premiers.size() >= 2) {
|
||||||
|
combPayables.addAll(CombinaisonUtil.allPairs(premiers));
|
||||||
|
}
|
||||||
|
|
||||||
|
// b) dead-heat seconds (positions[1] size >=2) -> all pairs combining each premier with each second
|
||||||
|
List<Integer> deuxiemes = positions.get(1);
|
||||||
|
if (deuxiemes.size() >= 1 && premiers.size() >= 1) {
|
||||||
|
for (Integer p : premiers) {
|
||||||
|
for (Integer d : deuxiemes) {
|
||||||
|
combPayables.add(new HashSet<>(Arrays.asList(p, d)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// c) if positions[0].size()==1 and positions[1].size()==1 and no dead-heat: comb = {1er,2e}
|
||||||
|
if (premiers.size() == 1 && deuxiemes.size() == 1) {
|
||||||
|
combPayables.add(new HashSet<>(Arrays.asList(premiers.get(0), deuxiemes.get(0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove combos involving non-partants -> these combos are remboursed (Article 4)
|
||||||
|
Set<Set<Integer>> combRemb = combPayables.stream()
|
||||||
|
.filter(c -> c.stream().anyMatch(nonPartants::contains))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// RNET = total des mises enregistrées sur la course
|
||||||
|
BigDecimal rnet = parisCourse.stream().map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
// MREMB = montant des mises sur combRemb
|
||||||
|
BigDecimal mremb = parisCourse.stream()
|
||||||
|
.filter(p -> combRemb.contains(stringToComb(p.getCombinaison())))
|
||||||
|
.map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
BigDecimal map = rnet.subtract(mremb).subtract(prelevements);
|
||||||
|
if (map.compareTo(BigDecimal.ZERO) < 0) map = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// Mises par combinaison (pour combPayables not remboursed)
|
||||||
|
Map<Set<Integer>, BigDecimal> misesParComb = new HashMap<>();
|
||||||
|
for (Set<Integer> comb : combPayables) {
|
||||||
|
if (combRemb.contains(comb)) {
|
||||||
|
misesParComb.put(comb, BigDecimal.ZERO);
|
||||||
|
} else {
|
||||||
|
BigDecimal s = parisCourse.stream()
|
||||||
|
.filter(p -> stringToComb(p.getCombinaison()).equals(comb))
|
||||||
|
.map(PariEntity::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
misesParComb.put(comb, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cas : aucune comb active => cagnotte
|
||||||
|
List<Set<Integer>> combActives = misesParComb.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
.map(Map.Entry::getKey).collect(Collectors.toList());
|
||||||
|
|
||||||
|
BigDecimal cagnotte = BigDecimal.ZERO;
|
||||||
|
Map<String, BigDecimal> gains = new HashMap<>();
|
||||||
|
|
||||||
|
if (combActives.isEmpty()) {
|
||||||
|
cagnotte = cagnotte.add(map);
|
||||||
|
} else if (combActives.size() == 1) {
|
||||||
|
// cas normal unique combinaison
|
||||||
|
Set<Integer> comb = combActives.get(0);
|
||||||
|
BigDecimal totMise = misesParComb.get(comb);
|
||||||
|
if (totMise.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
cagnotte = cagnotte.add(map);
|
||||||
|
} else {
|
||||||
|
BigDecimal rapport = map.divide(totMise, SCALE, RoundingMode.HALF_UP).add(BigDecimal.ONE);
|
||||||
|
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
|
||||||
|
// payer chaque parieur sur la combinaison
|
||||||
|
for (PariEntity p : parisCourse) {
|
||||||
|
if (stringToComb(p.getCombinaison()).equals(comb)) {
|
||||||
|
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// dead-heat / multiple combs payables
|
||||||
|
BigDecimal totalMisesOnCombActives = combActives.stream().map(misesParComb::get).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
BigDecimal benef = map.subtract(totalMisesOnCombActives);
|
||||||
|
if (benef.compareTo(BigDecimal.ZERO) < 0) benef = BigDecimal.ZERO;
|
||||||
|
int nbComb = combActives.size();
|
||||||
|
BigDecimal partParComb = benef.divide(BigDecimal.valueOf(nbComb), SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// allocate for each comb: partParComb distributed proportionally to mises on that comb
|
||||||
|
// but first for each comb we already have misesParComb.get(comb) that were removed from the map when computing benef
|
||||||
|
for (Set<Integer> comb : combActives) {
|
||||||
|
BigDecimal misesThis = misesParComb.get(comb);
|
||||||
|
if (misesThis.compareTo(BigDecimal.ZERO) == 0) continue;
|
||||||
|
BigDecimal ratio = partParComb.divide(misesThis, SCALE, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal rapport = ratio.add(BigDecimal.ONE);
|
||||||
|
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
|
||||||
|
for (PariEntity p : parisCourse) {
|
||||||
|
if (stringToComb(p.getCombinaison()).equals(comb)) {
|
||||||
|
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// traiter parts non couvertes (combActives avec mises 0): redistribuer their part among covered combs
|
||||||
|
BigDecimal partsNonCover = BigDecimal.ZERO;
|
||||||
|
for (Set<Integer> comb : combActives) {
|
||||||
|
if (misesParComb.get(comb).compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
partsNonCover = partsNonCover.add(partParComb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (partsNonCover.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal totalMisesCouvertes = combActives.stream()
|
||||||
|
.filter(c -> misesParComb.get(c).compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
.map(misesParComb::get).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
if (totalMisesCouvertes.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
cagnotte = cagnotte.add(map);
|
||||||
|
} else {
|
||||||
|
// redistribute partsNonCover to covered combos proportionally
|
||||||
|
for (Set<Integer> comb : combActives) {
|
||||||
|
BigDecimal misesThis = misesParComb.get(comb);
|
||||||
|
if (misesThis.compareTo(BigDecimal.ZERO) == 0) continue;
|
||||||
|
BigDecimal share = partsNonCover.multiply(misesThis).divide(totalMisesCouvertes, SCALE, RoundingMode.HALF_UP);
|
||||||
|
// distribute share proportionally to parieurs on the comb
|
||||||
|
for (PariEntity p : parisCourse) {
|
||||||
|
if (stringToComb(p.getCombinaison()).equals(comb)) {
|
||||||
|
BigDecimal extra = p.getMise().multiply(share).divide(misesThis, SCALE, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
gains.put(p.getParieur(), gains.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(extra));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build payment responses per record (each pari)
|
||||||
|
List<PaiementResponse> payments = new ArrayList<>();
|
||||||
|
for (PariEntity p : parisCourse) {
|
||||||
|
BigDecimal gain = gains.getOrDefault(p.getParieur(), BigDecimal.ZERO);
|
||||||
|
boolean gagnant = gain.compareTo(BigDecimal.ZERO) > 0;
|
||||||
|
payments.add(new PaiementResponse(p.getParieur(), gagnant, gain, p.getCombinaison()));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.put("paiements", payments);
|
||||||
|
out.put("cagnotte", cagnotte.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
out.put("rnet", rnet.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
out.put("mremb", mremb.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
out.put("map", map.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Integer> stringToComb(String s) {
|
||||||
|
return Arrays.stream(s.split(",")).map(Integer::valueOf).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.pmu.jumele.service;
|
||||||
|
|
||||||
|
import com.pmu.jumele.dto.PariRequest;
|
||||||
|
import com.pmu.jumele.dto.PositionsRequest;
|
||||||
|
import com.pmu.jumele.entity.PariEntity;
|
||||||
|
import com.pmu.jumele.repository.PariRepository;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//@DataJpaTest
|
||||||
|
public class JumeleGagnantServiceTest {
|
||||||
|
|
||||||
|
// @Autowired
|
||||||
|
PariRepository repo;
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
void test_simple_flow() {
|
||||||
|
JumeleGagnantService svc = new JumeleGagnantService(repo);
|
||||||
|
|
||||||
|
PariRequest pr = new PariRequest();
|
||||||
|
pr.setParieur("Alice");
|
||||||
|
pr.setCourseId("C1");
|
||||||
|
pr.setChevaux(Arrays.asList(1,2));
|
||||||
|
pr.setMise(500);
|
||||||
|
pr.setFormuleType("unitaire");
|
||||||
|
|
||||||
|
svc.enregistrerPari(pr);
|
||||||
|
|
||||||
|
// ajouter un autre pari sur la même comb
|
||||||
|
pr.setParieur("Bob");
|
||||||
|
svc.enregistrerPari(pr);
|
||||||
|
|
||||||
|
PositionsRequest pos = new PositionsRequest();
|
||||||
|
pos.setCourseId("C1");
|
||||||
|
pos.setPositions(List.of(List.of(1), List.of(2))); // 1er=1 ; 2e=2
|
||||||
|
|
||||||
|
Map<String,Object> res = svc.calculer(pos);
|
||||||
|
// assertThat(res).containsKey("paiements");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/com/pmu/jumele/util/CombinaisonUtil.java
Normal file
24
src/main/java/com/pmu/jumele/util/CombinaisonUtil.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.pmu.jumele.util;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class CombinaisonUtil {
|
||||||
|
|
||||||
|
// génère toutes paires (unordered) d'une liste de chevaux
|
||||||
|
public static Set<Set<Integer>> allPairs(List<Integer> chevaux) {
|
||||||
|
Set<Set<Integer>> res = new HashSet<>();
|
||||||
|
int n = chevaux.size();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
for (int j = i+1; j < n; j++) {
|
||||||
|
res.add(new HashSet<>(Arrays.asList(chevaux.get(i), chevaux.get(j))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// conversion Set<Integer> -> "a,b" sorted string
|
||||||
|
public static String combToString(Set<Integer> comb) {
|
||||||
|
return comb.stream().sorted().map(Object::toString).collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/java/com/pmu/jumele/util/FormulesTable.java
Normal file
35
src/main/java/com/pmu/jumele/util/FormulesTable.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.pmu.jumele.util;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class FormulesTable {
|
||||||
|
// For simplicity we store unit value per formula (complete combined) and simplified etc.
|
||||||
|
// We'll only need unit price to split a given "mise" into each unit bet when user submits a formula.
|
||||||
|
// Here we give reference table values per article 7 (unit values for "complete" formulas)
|
||||||
|
public static final Map<Integer, BigDecimal> COMBINED_COMPLETE = new HashMap<>();
|
||||||
|
public static final Map<Integer, BigDecimal> COMBINED_SIMPLE = new HashMap<>();
|
||||||
|
public static final Map<Integer, BigDecimal> CHAMP_TOTAL = new HashMap<>();
|
||||||
|
public static final Map<Integer, BigDecimal> CHAMP_PARTIEL = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// fill a subset (full table can be added)
|
||||||
|
COMBINED_COMPLETE.put(3, BigDecimal.valueOf(3000));
|
||||||
|
COMBINED_COMPLETE.put(4, BigDecimal.valueOf(6000));
|
||||||
|
COMBINED_COMPLETE.put(5, BigDecimal.valueOf(10000));
|
||||||
|
// ... add rest as needed
|
||||||
|
|
||||||
|
COMBINED_SIMPLE.put(3, BigDecimal.valueOf(1500));
|
||||||
|
COMBINED_SIMPLE.put(4, BigDecimal.valueOf(3000));
|
||||||
|
// ...
|
||||||
|
|
||||||
|
CHAMP_TOTAL.put(3, BigDecimal.valueOf(1000));
|
||||||
|
CHAMP_TOTAL.put(4, BigDecimal.valueOf(1500));
|
||||||
|
// ...
|
||||||
|
|
||||||
|
CHAMP_PARTIEL.put(3, BigDecimal.valueOf(1500));
|
||||||
|
CHAMP_PARTIEL.put(4, BigDecimal.valueOf(2000));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java
Normal file
13
src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmu.mali.apiplr;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class ApiPlrApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ApiPlrApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/java/com/pmu/mali/model/Pari.java
Normal file
16
src/main/java/com/pmu/mali/model/Pari.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.pmu.mali.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Pari {
|
||||||
|
private String parieur; // identifiant client ou ticket
|
||||||
|
private int numeroCheval; // numéro du cheval parié
|
||||||
|
private TypePari type; // SIMPLE_GAGNANT ou SIMPLE_PLACE
|
||||||
|
private BigDecimal mise; // montant mis (ex: 500)
|
||||||
|
private boolean nonPartant; // vrai si cheval non-partant (remboursement)
|
||||||
|
private String ecurieId; // identifiant d'écurie / coupling (null si aucun)
|
||||||
|
}
|
||||||
16
src/main/java/com/pmu/mali/model/ResultatCourse.java
Normal file
16
src/main/java/com/pmu/mali/model/ResultatCourse.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.pmu.mali.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ResultatCourse {
|
||||||
|
// listes des numéros de chevaux classés (peuvent contenir plusieurs en cas de dead-heat)
|
||||||
|
private List<Integer> premiers; // un ou plusieurs (dead-heat)
|
||||||
|
private List<Integer> deuxiemes; // peut être vide
|
||||||
|
private List<Integer> troisiemes; // peut être vide
|
||||||
|
private int nombrePartants; // nombre de chevaux engagés (programme officiel)
|
||||||
|
}
|
||||||
6
src/main/java/com/pmu/mali/model/TypePari.java
Normal file
6
src/main/java/com/pmu/mali/model/TypePari.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package com.pmu.mali.model;
|
||||||
|
|
||||||
|
public enum TypePari {
|
||||||
|
SIMPLE_GAGNANT,
|
||||||
|
SIMPLE_PLACE
|
||||||
|
}
|
||||||
367
src/main/java/com/pmu/mali/service/CalculPariService.java
Normal file
367
src/main/java/com/pmu/mali/service/CalculPariService.java
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
package com.pmu.mali.service;
|
||||||
|
|
||||||
|
import com.pmu.mali.model.Pari;
|
||||||
|
import com.pmu.mali.model.ResultatCourse;
|
||||||
|
import com.pmu.mali.model.TypePari;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de calcul des gains pour PARI SIMPLE (gagnant / placé)
|
||||||
|
* Implémente écurie, dead-heat, remboursements, cagnotte, rapport min.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CalculPariService {
|
||||||
|
|
||||||
|
private static final BigDecimal RAPPORT_MIN = BigDecimal.valueOf(1.1); // rapport minimum
|
||||||
|
private static final int SCALE = 6; // précision interne
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résultat retourné : map parieur -> gain (montant payé, inclut la mise).
|
||||||
|
* On retourne aussi une structure pour la cagnotte (optionnel).
|
||||||
|
*/
|
||||||
|
public static class ResultatCalcul {
|
||||||
|
public Map<String, BigDecimal> gainsParParieur = new HashMap<>();
|
||||||
|
public BigDecimal cagnotteGagnant = BigDecimal.ZERO;
|
||||||
|
public BigDecimal cagnottePlace = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point d'entrée principal.
|
||||||
|
*
|
||||||
|
* @param paris liste de paris (SIMPLE_GAGNANT et SIMPLE_PLACE mélangés)
|
||||||
|
* @param resultat résultat de la course
|
||||||
|
* @param prelevements montant total prélevé légalement (sur les deux types, si applicable)
|
||||||
|
* @return ResultatCalcul
|
||||||
|
*/
|
||||||
|
public ResultatCalcul calculer(List<Pari> paris, ResultatCourse resultat, BigDecimal prelevements) {
|
||||||
|
ResultatCalcul res = new ResultatCalcul();
|
||||||
|
|
||||||
|
if (prelevements == null) prelevements = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// Séparer paris par type
|
||||||
|
List<Pari> parisGagnant = filterByType(paris, TypePari.SIMPLE_GAGNANT);
|
||||||
|
List<Pari> parisPlace = filterByType(paris, TypePari.SIMPLE_PLACE);
|
||||||
|
|
||||||
|
// 1) Remboursement des non-partants
|
||||||
|
BigDecimal rembGagnant = rembourserNonPartants(parisGagnant, res);
|
||||||
|
BigDecimal rembPlace = rembourserNonPartants(parisPlace, res);
|
||||||
|
|
||||||
|
// 2) Calcul des masses à partager (RNET - MREMB - PRELEV)
|
||||||
|
BigDecimal masseGagnant = totalMise(parisGagnant).subtract(rembGagnant).subtract(prelevements);
|
||||||
|
BigDecimal massePlace = totalMise(parisPlace).subtract(rembPlace).subtract(prelevements);
|
||||||
|
|
||||||
|
if (masseGagnant.compareTo(BigDecimal.ZERO) < 0) masseGagnant = BigDecimal.ZERO;
|
||||||
|
if (massePlace.compareTo(BigDecimal.ZERO) < 0) massePlace = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// 3) Calcul gagnant (avec ecurie et dead-heat)
|
||||||
|
calculerGagnant(parisGagnant, resultat, masseGagnant, res);
|
||||||
|
|
||||||
|
// 4) Calcul placé (avec règles 4-7 / >=8 et dead-heat)
|
||||||
|
calculerPlace(parisPlace, resultat, massePlace, res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------
|
||||||
|
Méthodes utilitaires
|
||||||
|
---------------------- */
|
||||||
|
|
||||||
|
private List<Pari> filterByType(List<Pari> all, TypePari t) {
|
||||||
|
return all.stream().filter(p -> p.getType() == t).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal totalMise(List<Pari> list) {
|
||||||
|
return list.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rembourse toutes les mises dont pari.nonPartant == true
|
||||||
|
* Ajoute le montant remboursé aux gains du parieur.
|
||||||
|
* Retourne le total remboursé.
|
||||||
|
*/
|
||||||
|
private BigDecimal rembourserNonPartants(List<Pari> list, ResultatCalcul res) {
|
||||||
|
return rembourserNonPartants(list, res.gainsParParieur);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal rembourserNonPartants(List<Pari> list, Map<String, BigDecimal> gainsMap) {
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (Pari p : list) {
|
||||||
|
if (p.isNonPartant()) {
|
||||||
|
total = total.add(p.getMise());
|
||||||
|
gainsMap.put(p.getParieur(),
|
||||||
|
gainsMap.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(p.getMise()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------
|
||||||
|
Calcul du GAGNANT (inclut dead-heat/ecurie)
|
||||||
|
--------------------------------------- */
|
||||||
|
private void calculerGagnant(List<Pari> parisGagnant, ResultatCourse resultat, BigDecimal masse, ResultatCalcul res) {
|
||||||
|
List<Integer> premiers = resultat.getPremiers(); // liste des premiers (1 ou plusieurs)
|
||||||
|
if (premiers == null || premiers.isEmpty()) {
|
||||||
|
// Aucun cheval classé -> masse va en cagnotte
|
||||||
|
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si arrivée normale (un seul premier)
|
||||||
|
if (premiers.size() == 1) {
|
||||||
|
int premier = premiers.get(0);
|
||||||
|
// déterminer écurie du cheval gagnant (s'il y a), puis totaliser mises sur l'écurie
|
||||||
|
// On crée une clé d'ecurie : si pari.ecurieId == null, utilises le numéro du cheval comme "ecurie"
|
||||||
|
Map<String, BigDecimal> miseParEcurie = new HashMap<>();
|
||||||
|
for (Pari p : parisGagnant) {
|
||||||
|
if (p.isNonPartant()) continue;
|
||||||
|
String ecurie = keyEcurie(p);
|
||||||
|
// Si ce cheval appartient à l'écurie gagnante OU il est lui-même le gagnant, on prendra en compte
|
||||||
|
miseParEcurie.put(ecurie, miseParEcurie.getOrDefault(ecurie, BigDecimal.ZERO).add(p.getMise()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String ecurieGagnanteKey = null;
|
||||||
|
// trouver si le gagnant a une écurie dans les paris
|
||||||
|
Optional<Pari> anyPariSurGagnant = parisGagnant.stream()
|
||||||
|
.filter(p -> p.getNumeroCheval() == premier && !p.isNonPartant()).findAny();
|
||||||
|
if (anyPariSurGagnant.isPresent()) {
|
||||||
|
ecurieGagnanteKey = keyEcurie(anyPariSurGagnant.get());
|
||||||
|
} else {
|
||||||
|
// pas de mise sur le gagnant -> vérifier s'il y a mises sur autres chevaux de la même écurie
|
||||||
|
// chercher toute mise sur chevaux avec même ecurieId (si ecurie known)
|
||||||
|
// Si pas de mise du tout sur la combinaison gagnante => cagnotte
|
||||||
|
// So we'll compute totalMisePayable below
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculer total des mises payables (mises sur le cheval gagnant et, si écurie, sur ses co-écuries)
|
||||||
|
BigDecimal totalMisePayable = BigDecimal.ZERO;
|
||||||
|
if (ecurieGagnanteKey != null) {
|
||||||
|
totalMisePayable = miseParEcurie.getOrDefault(ecurieGagnanteKey, BigDecimal.ZERO);
|
||||||
|
} else {
|
||||||
|
// pas de pari identifié sur gagnant -> peut être 0
|
||||||
|
// cherché toutes mises portant exactement sur le cheval gagnant
|
||||||
|
totalMisePayable = parisGagnant.stream()
|
||||||
|
.filter(p -> p.getNumeroCheval() == premier && !p.isNonPartant())
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalMisePayable.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
// Aucun pari sur le gagnant (ni sur son ecurie) => vers cagnotte
|
||||||
|
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rapport brut = (masse / totalMisePayable) + 1 (on restitue la mise + bénéfice)
|
||||||
|
BigDecimal rapport = masse.divide(totalMisePayable, SCALE, RoundingMode.HALF_UP).add(BigDecimal.ONE);
|
||||||
|
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
|
||||||
|
|
||||||
|
// payer chaque parieur ayant parié sur ce cheval/ecurie
|
||||||
|
for (Pari p : parisGagnant) {
|
||||||
|
if (p.isNonPartant()) continue;
|
||||||
|
String key = keyEcurie(p);
|
||||||
|
boolean payable = false;
|
||||||
|
if (ecurieGagnanteKey != null) {
|
||||||
|
// payables si même ecurie
|
||||||
|
payable = ecurieGagnanteKey.equals(key);
|
||||||
|
} else {
|
||||||
|
payable = (p.getNumeroCheval() == premier);
|
||||||
|
}
|
||||||
|
if (payable) {
|
||||||
|
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
res.gainsParParieur.put(p.getParieur(),
|
||||||
|
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
* Cas dead-heat (plusieurs premiers)
|
||||||
|
* ------------------------- */
|
||||||
|
// 1) calculer total mise sur les chevaux payables (les chevaux classés premiers)
|
||||||
|
// Règle du règlement : "le montant de toutes les mises sur les divers chevaux payables est d’abord retiré de la masse à partager."
|
||||||
|
BigDecimal misesSurPayables = parisGagnant.stream()
|
||||||
|
.filter(p -> !p.isNonPartant() && premiers.contains(p.getNumeroCheval()))
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
// Bénéfice à répartir
|
||||||
|
BigDecimal benefice = masse.subtract(misesSurPayables);
|
||||||
|
if (benefice.compareTo(BigDecimal.ZERO) < 0) benefice = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
int nbChevauxPremiers = premiers.size();
|
||||||
|
if (nbChevauxPremiers == 0) {
|
||||||
|
res.cagnotteGagnant = res.cagnotteGagnant.add(masse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diviser le bénéfice en autant de parts qu'il y a de chevaux classés premiers
|
||||||
|
BigDecimal partParCheval = benefice.divide(BigDecimal.valueOf(nbChevauxPremiers), SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// Pour chaque cheval premier : répartir sa part au prorata des mises sur ce cheval
|
||||||
|
for (Integer cheval : premiers) {
|
||||||
|
// mises sur ce cheval
|
||||||
|
BigDecimal misesSurCheval = parisGagnant.stream()
|
||||||
|
.filter(p -> !p.isNonPartant() && p.getNumeroCheval() == cheval)
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
if (misesSurCheval.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
// si aucune mise sur ce cheval, sa part est redistribuée entre les autres premiers
|
||||||
|
// impl: ajouter cette part à cagnotte temporaire pour redistribution
|
||||||
|
// pour simplicité on ajoute à cagnotte et on répartira après
|
||||||
|
res.cagnotteGagnant = res.cagnotteGagnant.add(partParCheval);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// montant additionnel par unité de mise = partParCheval / misesSurCheval
|
||||||
|
BigDecimal ratio = partParCheval.divide(misesSurCheval, SCALE, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal rapport = ratio.add(BigDecimal.ONE); // ajoute la mise
|
||||||
|
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
|
||||||
|
|
||||||
|
// payer chaque parieur sur ce cheval
|
||||||
|
for (Pari p : parisGagnant) {
|
||||||
|
if (p.isNonPartant()) continue;
|
||||||
|
if (p.getNumeroCheval() == cheval) {
|
||||||
|
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
res.gainsParParieur.put(p.getParieur(),
|
||||||
|
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
|
||||||
|
} else {
|
||||||
|
// si écurie: si cheval p appartient à écurie du cheval premier, il peut avoir droit (règle d'écurie)
|
||||||
|
// on doit totaliser mises par écurie gagnante et partager la part correspondante.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: gestion complète des écuries dans dead-heat nécessite agrégation par ecurie + redistribution des parts relatives.
|
||||||
|
// Pour respecter à la lettre, on devrait :
|
||||||
|
// - agréger mises par ecurie pour les chevaux premiers,
|
||||||
|
// - distribuer la partParCheval à l'ecurie, puis parmi les chevaux de l'ecurie proportionnellement,
|
||||||
|
// - ci-dessus on a fait la distribution cheval-par-cheval (simplifié).
|
||||||
|
}
|
||||||
|
|
||||||
|
private String keyEcurie(Pari p) {
|
||||||
|
if (p.getEcurieId() != null && !p.getEcurieId().trim().isEmpty()) {
|
||||||
|
return "E:" + p.getEcurieId();
|
||||||
|
} else {
|
||||||
|
return "H:" + p.getNumeroCheval(); // seul cheval = sa "propre ecurie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------
|
||||||
|
* Calcul du PLACE (inclut dead-heat/ecurie)
|
||||||
|
* --------------------------------------- */
|
||||||
|
private void calculerPlace(List<Pari> parisPlace, ResultatCourse resultat, BigDecimal masse, ResultatCalcul res) {
|
||||||
|
// déterminer horses payable: si >=8 partants => 3 places payées, si 4-7 => 2, sinon mise en cagnotte (article)
|
||||||
|
int nPartants = resultat.getNombrePartants();
|
||||||
|
int nbPlacesPayees;
|
||||||
|
if (nPartants >= 8) nbPlacesPayees = 3;
|
||||||
|
else if (nPartants >= 4) nbPlacesPayees = 2;
|
||||||
|
else {
|
||||||
|
// si moins de 4 partants, la masse place va en cagnotte
|
||||||
|
res.cagnottePlace = res.cagnottePlace.add(masse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construction des chevaux payables selon les listes de résultat :
|
||||||
|
// Remarque : listes peuvent contenir dead-heats (plusieurs elements)
|
||||||
|
List<Integer> payables = new ArrayList<>();
|
||||||
|
if (nbPlacesPayees == 2) {
|
||||||
|
payables.addAll(resultat.getPremiers());
|
||||||
|
payables.addAll(resultat.getDeuxiemes());
|
||||||
|
} else {
|
||||||
|
// 3 places
|
||||||
|
payables.addAll(resultat.getPremiers());
|
||||||
|
payables.addAll(resultat.getDeuxiemes());
|
||||||
|
payables.addAll(resultat.getTroisiemes());
|
||||||
|
}
|
||||||
|
// on conserve distincts tout en gardant l'ordre logique
|
||||||
|
LinkedHashSet<Integer> set = new LinkedHashSet<>(payables);
|
||||||
|
List<Integer> chevauxPayables = new ArrayList<>(set);
|
||||||
|
|
||||||
|
if (chevauxPayables.isEmpty()) {
|
||||||
|
res.cagnottePlace = res.cagnottePlace.add(masse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retirer "le montant de toutes les mises sur les divers chevaux payables" de la masse
|
||||||
|
BigDecimal miseSurPayables = parisPlace.stream()
|
||||||
|
.filter(p -> !p.isNonPartant() && chevauxPayables.contains(p.getNumeroCheval()))
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
BigDecimal benefice = masse.subtract(miseSurPayables);
|
||||||
|
if (benefice.compareTo(BigDecimal.ZERO) < 0) benefice = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// Le bénéfice est divisé en autant de parts égales qu'il y a de "combinaisons payables".
|
||||||
|
// Ici on considère "combinaisons payables" = nombre de positions payées (nbPlacesPayees)
|
||||||
|
// -> chaque part ensuite partagée proportionnellement entre chevaux payables de la position concernée.
|
||||||
|
BigDecimal partParCombinaison = benefice.divide(BigDecimal.valueOf(nbPlacesPayees), SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// Répartition : pour chaque position (1er, 2e, 3e) on divise la part entre les chevaux classés à cette position
|
||||||
|
// puis, pour chaque cheval, on partage au prorata de ses mises sur cette position parmi les chevaux payables de la position.
|
||||||
|
// Simplification pratique: on va parcourir les positions et appliquer.
|
||||||
|
// Position 1
|
||||||
|
List<Integer> premiers = resultat.getPremiers();
|
||||||
|
repartirPlacePosition(parisPlace, premiers, partParCombinaison, res);
|
||||||
|
|
||||||
|
if (nbPlacesPayees >= 2) {
|
||||||
|
List<Integer> deuxiemes = resultat.getDeuxiemes();
|
||||||
|
repartirPlacePosition(parisPlace, deuxiemes, partParCombinaison, res);
|
||||||
|
}
|
||||||
|
if (nbPlacesPayees >= 3) {
|
||||||
|
List<Integer> troisiemes = resultat.getTroisiemes();
|
||||||
|
repartirPlacePosition(parisPlace, troisiemes, partParCombinaison, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note : si pour une position donnée il n'y a aucune mise, selon le règlement sa part est répartie entre les autres chevaux payables.
|
||||||
|
// Ici, si aucune mise sur une position entière, on ajoute cette part à la cagnottePlace (impl simplifiée).
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repartirPlacePosition(List<Pari> parisPlace, List<Integer> chevauxPosition, BigDecimal partParCombinaison, ResultatCalcul res) {
|
||||||
|
if (chevauxPosition == null || chevauxPosition.isEmpty()) {
|
||||||
|
// pas de cheval classé à cette position -> part va en cagnotte
|
||||||
|
res.cagnottePlace = res.cagnottePlace.add(partParCombinaison);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// total mises sur les chevaux de cette position
|
||||||
|
BigDecimal totalMises = parisPlace.stream()
|
||||||
|
.filter(p -> !p.isNonPartant() && chevauxPosition.contains(p.getNumeroCheval()))
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
if (totalMises.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
// aucune mise sur cette position -> part est ajoutée à cagnotte pour redistribution (impl simplifiée)
|
||||||
|
res.cagnottePlace = res.cagnottePlace.add(partParCombinaison);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// chaque cheval recevra une fraction proportionnelle de la partParCombinaison
|
||||||
|
for (Integer cheval : chevauxPosition) {
|
||||||
|
BigDecimal misesSurCheval = parisPlace.stream()
|
||||||
|
.filter(p -> !p.isNonPartant() && p.getNumeroCheval() == cheval)
|
||||||
|
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
if (misesSurCheval.compareTo(BigDecimal.ZERO) == 0) continue;
|
||||||
|
|
||||||
|
// montant additionnel pour ce cheval = partParCombinaison * (misesSurCheval / totalMises)
|
||||||
|
BigDecimal share = partParCombinaison.multiply(misesSurCheval).divide(totalMises, SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// Le rapport brut pour chaque mise sur ce cheval = (share / misesSurCheval) + 1
|
||||||
|
BigDecimal ratio = share.divide(misesSurCheval, SCALE, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal rapport = ratio.add(BigDecimal.ONE);
|
||||||
|
if (rapport.compareTo(RAPPORT_MIN) < 0) rapport = RAPPORT_MIN;
|
||||||
|
|
||||||
|
// appliquer paiement à tous les parieurs sur ce cheval
|
||||||
|
for (Pari p : parisPlace) {
|
||||||
|
if (p.isNonPartant()) continue;
|
||||||
|
if (p.getNumeroCheval() == cheval) {
|
||||||
|
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
res.gainsParParieur.put(p.getParieur(),
|
||||||
|
res.gainsParParieur.getOrDefault(p.getParieur(), BigDecimal.ZERO).add(gain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// CourseController.java
|
||||||
|
package com.pmumali.jumeleordre.controller;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.dto.ResultatCourseDto;
|
||||||
|
import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException;
|
||||||
|
import com.pmumali.jumeleordre.model.Course;
|
||||||
|
import com.pmumali.jumeleordre.service.ResultatCourseService;
|
||||||
|
import com.pmumali.jumeleordre.service.CourseService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/courses")
|
||||||
|
public class CourseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CourseService courseService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResultatCourseService resultatCourseService;
|
||||||
|
|
||||||
|
@GetMapping("/actives")
|
||||||
|
public ResponseEntity<List<Course>> getCoursesActives() {
|
||||||
|
List<Course> courses = courseService.getCoursesActives();
|
||||||
|
return ResponseEntity.ok(courses);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resultat")
|
||||||
|
public ResponseEntity<Void> soumettreResultatCourse(@RequestBody ResultatCourseDto resultatDto) {
|
||||||
|
try {
|
||||||
|
resultatCourseService.traiterResultatCourse(resultatDto);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (ResultatCourseInvalideException e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// ParisController.java
|
||||||
|
package com.pmumali.jumeleordre.controller;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.dto.ParisDto;
|
||||||
|
import com.pmumali.jumeleordre.dto.GainsDto;
|
||||||
|
import com.pmumali.jumeleordre.exception.ParisInvalideException;
|
||||||
|
import com.pmumali.jumeleordre.model.Paris;
|
||||||
|
import com.pmumali.jumeleordre.service.ParisService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/paris")
|
||||||
|
public class ParisController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ParisService parisService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Paris> enregistrerParis(@RequestBody ParisDto parisDto) {
|
||||||
|
try {
|
||||||
|
Paris paris = parisService.enregistrerParis(parisDto);
|
||||||
|
return ResponseEntity.ok(paris);
|
||||||
|
} catch (ParisInvalideException e) {
|
||||||
|
return ResponseEntity.badRequest().body(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/parieur/{idParieur}")
|
||||||
|
public ResponseEntity<List<Paris>> getParisParParieur(@PathVariable String idParieur) {
|
||||||
|
List<Paris> paris = parisService.getParisParParieur(idParieur);
|
||||||
|
return ResponseEntity.ok(paris);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/gains/{idParis}")
|
||||||
|
public ResponseEntity<GainsDto> getGains(@PathVariable Long idParis) {
|
||||||
|
// Implémentation pour obtenir les détails des gains d'un pari
|
||||||
|
return ResponseEntity.ok(new GainsDto());
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java
Normal file
11
src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// GainsDto.java
|
||||||
|
package com.pmumali.jumeleordre.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GainsDto {
|
||||||
|
private Long idParis;
|
||||||
|
private double montant;
|
||||||
|
private String statut;
|
||||||
|
}
|
||||||
13
src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java
Normal file
13
src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// ParisDto.java
|
||||||
|
package com.pmumali.jumeleordre.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ParisDto {
|
||||||
|
private Long idCourse;
|
||||||
|
private Long idChevalPremier;
|
||||||
|
private Long idChevalDeuxieme;
|
||||||
|
private double mise;
|
||||||
|
private String idParieur;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// ResultatCourseDto.java
|
||||||
|
package com.pmumali.jumeleordre.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ResultatCourseDto {
|
||||||
|
private Long idCourse;
|
||||||
|
private Long idChevalPremier;
|
||||||
|
private Long idChevalDeuxieme;
|
||||||
|
private List<Long> chevauxDeadHeat;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// ChevalInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class ChevalInvalideException extends JumeleOrdreException {
|
||||||
|
public ChevalInvalideException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChevalInvalideException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// CourseDejaTermineeException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class CourseDejaTermineeException extends CourseInvalideException {
|
||||||
|
public CourseDejaTermineeException(Long idCourse) {
|
||||||
|
super(String.format("La course avec l'ID %d est déjà terminée", idCourse));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// CourseInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class CourseInvalideException extends JumeleOrdreException {
|
||||||
|
public CourseInvalideException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CourseInvalideException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(JumeleOrdreException.class)
|
||||||
|
public ResponseEntity<Object> handleJumeleOrdreException(
|
||||||
|
JumeleOrdreException ex, WebRequest request) {
|
||||||
|
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("timestamp", LocalDateTime.now());
|
||||||
|
body.put("message", ex.getMessage());
|
||||||
|
body.put("type", ex.getClass().getSimpleName());
|
||||||
|
|
||||||
|
HttpStatus status = HttpStatus.BAD_REQUEST;
|
||||||
|
|
||||||
|
if (ex instanceof CourseDejaTermineeException) {
|
||||||
|
status = HttpStatus.CONFLICT;
|
||||||
|
} else if (ex instanceof LimiteMiseDepasseeException) {
|
||||||
|
LimiteMiseDepasseeException specificEx = (LimiteMiseDepasseeException) ex;
|
||||||
|
body.put("miseActuelle", specificEx.getMiseActuelle());
|
||||||
|
body.put("miseMaximum", specificEx.getMiseMaximum());
|
||||||
|
} else if (ex instanceof ValidationException) {
|
||||||
|
ValidationException validationEx = (ValidationException) ex;
|
||||||
|
body.put("erreurs", validationEx.getErreurs());
|
||||||
|
status = HttpStatus.UNPROCESSABLE_ENTITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResponseEntity<>(body, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Object> handleGlobalException(
|
||||||
|
Exception ex, WebRequest request) {
|
||||||
|
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("timestamp", LocalDateTime.now());
|
||||||
|
body.put("message", "Une erreur interne est survenue");
|
||||||
|
body.put("type", "ErreurInterne");
|
||||||
|
|
||||||
|
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class JumeleOrdreException extends RuntimeException {
|
||||||
|
public JumeleOrdreException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JumeleOrdreException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// LimiteMiseDepasseeException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class LimiteMiseDepasseeException extends ParisInvalideException {
|
||||||
|
private final double miseActuelle;
|
||||||
|
private final double miseMaximum;
|
||||||
|
|
||||||
|
public LimiteMiseDepasseeException(double miseActuelle, double miseMaximum) {
|
||||||
|
super(String.format("La mise totale de %.2f FCFA dépasse la limite autorisée de %.2f FCFA",
|
||||||
|
miseActuelle, miseMaximum));
|
||||||
|
this.miseActuelle = miseActuelle;
|
||||||
|
this.miseMaximum = miseMaximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMiseActuelle() {
|
||||||
|
return miseActuelle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMiseMaximum() {
|
||||||
|
return miseMaximum;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// NombreChevauxInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class NombreChevauxInvalideException extends CourseInvalideException {
|
||||||
|
private final int nombreChevaux;
|
||||||
|
|
||||||
|
public NombreChevauxInvalideException(int nombreChevaux) {
|
||||||
|
super(String.format("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre (nombre actuel: %d)",
|
||||||
|
nombreChevaux));
|
||||||
|
this.nombreChevaux = nombreChevaux;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNombreChevaux() {
|
||||||
|
return nombreChevaux;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// PaiementInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class PaiementInvalideException extends JumeleOrdreException {
|
||||||
|
public PaiementInvalideException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaiementInvalideException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// ParisInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class ParisInvalideException extends JumeleOrdreException {
|
||||||
|
public ParisInvalideException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParisInvalideException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// ResultatCourseInvalideException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
public class ResultatCourseInvalideException extends JumeleOrdreException {
|
||||||
|
public ResultatCourseInvalideException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultatCourseInvalideException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// ValidationException.java
|
||||||
|
package com.pmumali.jumeleordre.exception;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ValidationException extends JumeleOrdreException {
|
||||||
|
private final List<String> erreurs;
|
||||||
|
|
||||||
|
public ValidationException(String message, List<String> erreurs) {
|
||||||
|
super(message);
|
||||||
|
this.erreurs = erreurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getErreurs() {
|
||||||
|
return erreurs;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/com/pmumali/jumeleordre/model/Cheval.java
Normal file
17
src/main/java/com/pmumali/jumeleordre/model/Cheval.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.pmumali.jumeleordre.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
public class Cheval {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
private String nom;
|
||||||
|
private boolean nonPartant;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Course course;
|
||||||
|
}
|
||||||
23
src/main/java/com/pmumali/jumeleordre/model/Course.java
Normal file
23
src/main/java/com/pmumali/jumeleordre/model/Course.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Course.java
|
||||||
|
package com.pmumali.jumeleordre.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
public class Course {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
private String nom;
|
||||||
|
private LocalDateTime heureDebut;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
|
||||||
|
private List<Cheval> chevaux;
|
||||||
|
private boolean estTerminee;
|
||||||
|
private boolean aDeadHeat;
|
||||||
|
}
|
||||||
34
src/main/java/com/pmumali/jumeleordre/model/Paris.java
Normal file
34
src/main/java/com/pmumali/jumeleordre/model/Paris.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Paris.java
|
||||||
|
package com.pmumali.jumeleordre.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
public class Paris {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Cheval premier;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Cheval deuxieme;
|
||||||
|
|
||||||
|
private double mise;
|
||||||
|
private LocalDateTime dateParis;
|
||||||
|
private String idParieur;
|
||||||
|
private StatutParis statut;
|
||||||
|
private Double gains;
|
||||||
|
|
||||||
|
public enum StatutParis {
|
||||||
|
EN_ATTENTE, GAGNANT, PERDANT, REMBOURSE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// ResultatCourse.java
|
||||||
|
package com.pmumali.jumeleordre.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
public class ResultatCourse {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Cheval premier;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Cheval deuxieme;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
private List<Long> chevauxDeadHeat;
|
||||||
|
|
||||||
|
private double totalMises;
|
||||||
|
private double masseAPartager;
|
||||||
|
private double prelevementsLegaux;
|
||||||
|
private double montantRembourse;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// ChevalRepository.java
|
||||||
|
package com.pmumali.jumeleordre.repository;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.model.Cheval;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// CourseRepository.java
|
||||||
|
package com.pmumali.jumeleordre.repository;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.model.Course;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CourseRepository extends JpaRepository<Course, Long> {
|
||||||
|
List<Course> findByEstTermineeFalse();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// ParisRepository.java
|
||||||
|
package com.pmumali.jumeleordre.repository;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.model.Paris;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ParisRepository extends JpaRepository<Paris, Long> {
|
||||||
|
List<Paris> findByCourseIdAndStatut(Long idCourse, Paris.StatutParis statut);
|
||||||
|
List<Paris> findByIdParieur(String idParieur);
|
||||||
|
|
||||||
|
List<Paris> findByCourseId(Long courseID);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// ResultatCourseRepository.java
|
||||||
|
package com.pmumali.jumeleordre.repository;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.model.ResultatCourse;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
|
||||||
|
ResultatCourse findByCourseId(Long idCourse);
|
||||||
|
}
|
||||||
142
src/main/java/com/pmumali/jumeleordre/service/CourseService.java
Normal file
142
src/main/java/com/pmumali/jumeleordre/service/CourseService.java
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package com.pmumali.jumeleordre.service;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.model.Course;
|
||||||
|
import com.pmumali.jumeleordre.model.Cheval;
|
||||||
|
import com.pmumali.jumeleordre.repository.CourseRepository;
|
||||||
|
import com.pmumali.jumeleordre.repository.ChevalRepository;
|
||||||
|
import com.pmumali.jumeleordre.exception.CourseInvalideException;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CourseService {
|
||||||
|
|
||||||
|
private final CourseRepository courseRepository;
|
||||||
|
private final ChevalRepository chevalRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CourseService(CourseRepository courseRepository, ChevalRepository chevalRepository) {
|
||||||
|
this.courseRepository = courseRepository;
|
||||||
|
this.chevalRepository = chevalRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les courses actives (non terminées)
|
||||||
|
*/
|
||||||
|
public List<Course> getCoursesActives() {
|
||||||
|
return courseRepository.findByEstTermineeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle course
|
||||||
|
*/
|
||||||
|
public Course creerCourse(String nomCourse, LocalDateTime heureDebut, List<Long> idsChevaux) throws CourseInvalideException {
|
||||||
|
if (idsChevaux.size() > 7) {
|
||||||
|
throw new CourseInvalideException("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre");
|
||||||
|
}
|
||||||
|
|
||||||
|
Course course = new Course();
|
||||||
|
course.setNom(nomCourse);
|
||||||
|
course.setHeureDebut(heureDebut);
|
||||||
|
course.setEstTerminee(false);
|
||||||
|
course.setADeadHeat(false);
|
||||||
|
|
||||||
|
Course savedCourse = courseRepository.save(course);
|
||||||
|
|
||||||
|
// Associer les chevaux à la course
|
||||||
|
List<Cheval> chevaux = chevalRepository.findAllById(idsChevaux);
|
||||||
|
for (Cheval cheval : chevaux) {
|
||||||
|
cheval.setCourse(savedCourse);
|
||||||
|
chevalRepository.save(cheval);
|
||||||
|
}
|
||||||
|
|
||||||
|
savedCourse.setChevaux(chevaux);
|
||||||
|
return courseRepository.save(savedCourse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une course par son ID
|
||||||
|
*/
|
||||||
|
public Course getCourseById(Long id) throws CourseInvalideException {
|
||||||
|
return courseRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new CourseInvalideException("Course non trouvée avec l'ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les informations d'une course
|
||||||
|
*/
|
||||||
|
public Course mettreAJourCourse(Long id, String nom, LocalDateTime heureDebut) throws CourseInvalideException {
|
||||||
|
Course course = getCourseById(id);
|
||||||
|
|
||||||
|
if (nom != null) {
|
||||||
|
course.setNom(nom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heureDebut != null) {
|
||||||
|
course.setHeureDebut(heureDebut);
|
||||||
|
}
|
||||||
|
|
||||||
|
return courseRepository.save(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un cheval à une course
|
||||||
|
*/
|
||||||
|
public Course ajouterChevalACourse(Long idCourse, Long idCheval) throws CourseInvalideException {
|
||||||
|
Course course = getCourseById(idCourse);
|
||||||
|
Cheval cheval = chevalRepository.findById(idCheval)
|
||||||
|
.orElseThrow(() -> new CourseInvalideException("Cheval non trouvé avec l'ID: " + idCheval));
|
||||||
|
|
||||||
|
if (course.getChevaux().size() >= 7) {
|
||||||
|
throw new CourseInvalideException("Une course ne peut pas avoir plus de 7 chevaux pour le pari Jumelé Ordre");
|
||||||
|
}
|
||||||
|
|
||||||
|
cheval.setCourse(course);
|
||||||
|
chevalRepository.save(cheval);
|
||||||
|
|
||||||
|
course.getChevaux().add(cheval);
|
||||||
|
return courseRepository.save(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque un cheval comme non partant
|
||||||
|
*/
|
||||||
|
public Cheval declarerChevalNonPartant(Long idCheval) throws CourseInvalideException {
|
||||||
|
Cheval cheval = chevalRepository.findById(idCheval)
|
||||||
|
.orElseThrow(() -> new CourseInvalideException("Cheval non trouvé avec l'ID: " + idCheval));
|
||||||
|
|
||||||
|
cheval.setNonPartant(true);
|
||||||
|
return chevalRepository.save(cheval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une course (si elle n'a pas encore commencé)
|
||||||
|
*/
|
||||||
|
public void supprimerCourse(Long id) throws CourseInvalideException {
|
||||||
|
Course course = getCourseById(id);
|
||||||
|
|
||||||
|
if (course.getHeureDebut().isBefore(LocalDateTime.now())) {
|
||||||
|
throw new CourseInvalideException("Impossible de supprimer une course qui a déjà commencé");
|
||||||
|
}
|
||||||
|
|
||||||
|
courseRepository.delete(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les courses
|
||||||
|
*/
|
||||||
|
public List<Course> getAllCourses() {
|
||||||
|
return courseRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si une course existe
|
||||||
|
*/
|
||||||
|
public boolean courseExiste(Long id) {
|
||||||
|
return courseRepository.existsById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// ParisService.java
|
||||||
|
package com.pmumali.jumeleordre.service;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.dto.ParisDto;
|
||||||
|
import com.pmumali.jumeleordre.exception.ParisInvalideException;
|
||||||
|
import com.pmumali.jumeleordre.model.Cheval;
|
||||||
|
import com.pmumali.jumeleordre.model.Course;
|
||||||
|
import com.pmumali.jumeleordre.model.Paris;
|
||||||
|
import com.pmumali.jumeleordre.repository.ChevalRepository;
|
||||||
|
import com.pmumali.jumeleordre.repository.CourseRepository;
|
||||||
|
import com.pmumali.jumeleordre.repository.ParisRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ParisService {
|
||||||
|
|
||||||
|
private static final double MISE_MINIMUM = 500;
|
||||||
|
private static final double MISE_MAXIMUM_PAR_PARIS = 20 * MISE_MINIMUM;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ParisRepository parisRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CourseRepository courseRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChevalRepository chevalRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Paris enregistrerParis(ParisDto parisDto) throws ParisInvalideException {
|
||||||
|
Course course = courseRepository.findById(parisDto.getIdCourse())
|
||||||
|
.orElseThrow(() -> new ParisInvalideException("Course non trouvée"));
|
||||||
|
|
||||||
|
if (course.isEstTerminee()) {
|
||||||
|
throw new ParisInvalideException("Impossible de parier sur une course terminée");
|
||||||
|
}
|
||||||
|
|
||||||
|
Cheval premier = chevalRepository.findById(parisDto.getIdChevalPremier())
|
||||||
|
.orElseThrow(() -> new ParisInvalideException("Cheval premier non trouvé"));
|
||||||
|
|
||||||
|
Cheval deuxieme = chevalRepository.findById(parisDto.getIdChevalDeuxieme())
|
||||||
|
.orElseThrow(() -> new ParisInvalideException("Cheval deuxième non trouvé"));
|
||||||
|
|
||||||
|
if (premier.equals(deuxieme)) {
|
||||||
|
throw new ParisInvalideException("Impossible de parier sur le même cheval pour les deux positions");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (premier.isNonPartant() || deuxieme.isNonPartant()) {
|
||||||
|
throw new ParisInvalideException("Impossible de parier sur des chevaux non partants");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parisDto.getMise() < MISE_MINIMUM) {
|
||||||
|
throw new ParisInvalideException("La mise doit être d'au moins " + MISE_MINIMUM + " FCFA");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du total des mises sur cette combinaison
|
||||||
|
double totalMisesCombinaison = parisRepository.findByCourseIdAndStatut(course.getId(), Paris.StatutParis.EN_ATTENTE)
|
||||||
|
.stream()
|
||||||
|
.filter(p -> p.getPremier().equals(premier) && p.getDeuxieme().equals(deuxieme))
|
||||||
|
.mapToDouble(Paris::getMise)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
if (totalMisesCombinaison + parisDto.getMise() > MISE_MAXIMUM_PAR_PARIS) {
|
||||||
|
throw new ParisInvalideException("Mise maximale pour cette combinaison dépassée");
|
||||||
|
}
|
||||||
|
|
||||||
|
Paris paris = new Paris();
|
||||||
|
paris.setCourse(course);
|
||||||
|
paris.setPremier(premier);
|
||||||
|
paris.setDeuxieme(deuxieme);
|
||||||
|
paris.setMise(parisDto.getMise());
|
||||||
|
paris.setDateParis(LocalDateTime.now());
|
||||||
|
paris.setIdParieur(parisDto.getIdParieur());
|
||||||
|
paris.setStatut(Paris.StatutParis.EN_ATTENTE);
|
||||||
|
|
||||||
|
return parisRepository.save(paris);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Paris> getParisParParieur(String idParieur) {
|
||||||
|
return parisRepository.findByIdParieur(idParieur);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// ResultatCourseService.java
|
||||||
|
package com.pmumali.jumeleordre.service;
|
||||||
|
|
||||||
|
import com.pmumali.jumeleordre.dto.ResultatCourseDto;
|
||||||
|
import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException;
|
||||||
|
import com.pmumali.jumeleordre.model.*;
|
||||||
|
import com.pmumali.jumeleordre.repository.*;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ResultatCourseService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CourseRepository courseRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChevalRepository chevalRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ParisRepository parisRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResultatCourseRepository resultatCourseRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void traiterResultatCourse(ResultatCourseDto resultatDto) throws ResultatCourseInvalideException {
|
||||||
|
Course course = courseRepository.findById(resultatDto.getIdCourse())
|
||||||
|
.orElseThrow(() -> new ResultatCourseInvalideException("Course non trouvée"));
|
||||||
|
|
||||||
|
if (course.isEstTerminee()) {
|
||||||
|
throw new ResultatCourseInvalideException("Résultats déjà traités pour cette course");
|
||||||
|
}
|
||||||
|
|
||||||
|
Cheval premier = chevalRepository.findById(resultatDto.getIdChevalPremier())
|
||||||
|
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval premier non trouvé"));
|
||||||
|
|
||||||
|
Cheval deuxieme = chevalRepository.findById(resultatDto.getIdChevalDeuxieme())
|
||||||
|
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval deuxième non trouvé"));
|
||||||
|
|
||||||
|
// Validation des chevaux en dead heat
|
||||||
|
if (resultatDto.getChevauxDeadHeat() != null) {
|
||||||
|
for (Long idCheval : resultatDto.getChevauxDeadHeat()) {
|
||||||
|
chevalRepository.findById(idCheval)
|
||||||
|
.orElseThrow(() -> new ResultatCourseInvalideException("Cheval en dead heat non trouvé"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrement du résultat
|
||||||
|
ResultatCourse resultat = new ResultatCourse();
|
||||||
|
resultat.setCourse(course);
|
||||||
|
resultat.setPremier(premier);
|
||||||
|
resultat.setDeuxieme(deuxieme);
|
||||||
|
resultat.setChevauxDeadHeat(resultatDto.getChevauxDeadHeat());
|
||||||
|
|
||||||
|
// Calcul des totaux et déductions
|
||||||
|
List<Paris> tousParis = parisRepository.findByCourseId(course.getId());
|
||||||
|
|
||||||
|
double totalMises = tousParis.stream().mapToDouble(Paris::getMise).sum();
|
||||||
|
double prelevementsLegaux = calculerPrelevementsLegaux(totalMises);
|
||||||
|
double montantRembourse = calculerMontantRembourse(tousParis);
|
||||||
|
|
||||||
|
double masseAPartager = totalMises - prelevementsLegaux - montantRembourse;
|
||||||
|
|
||||||
|
resultat.setTotalMises(totalMises);
|
||||||
|
resultat.setPrelevementsLegaux(prelevementsLegaux);
|
||||||
|
resultat.setMontantRembourse(montantRembourse);
|
||||||
|
resultat.setMasseAPartager(masseAPartager);
|
||||||
|
|
||||||
|
resultatCourseRepository.save(resultat);
|
||||||
|
|
||||||
|
// Traitement des paris
|
||||||
|
traiterParis(tousParis, resultat);
|
||||||
|
|
||||||
|
// Marquer la course comme terminée
|
||||||
|
course.setEstTerminee(true);
|
||||||
|
course.setADeadHeat(resultatDto.getChevauxDeadHeat() != null && !resultatDto.getChevauxDeadHeat().isEmpty());
|
||||||
|
courseRepository.save(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculerPrelevementsLegaux(double totalMises) {
|
||||||
|
// Implémentation selon les exigences légales
|
||||||
|
return totalMises * 0.10; // Exemple: 10% de prélèvement
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculerMontantRembourse(List<Paris> paris) {
|
||||||
|
return paris.stream()
|
||||||
|
.filter(p -> p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant())
|
||||||
|
.mapToDouble(Paris::getMise)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traiterParis(List<Paris> paris, ResultatCourse resultat) {
|
||||||
|
boolean aDeadHeat = resultat.getChevauxDeadHeat() != null && !resultat.getChevauxDeadHeat().isEmpty();
|
||||||
|
|
||||||
|
for (Paris p : paris) {
|
||||||
|
// Vérification des non-partants (Article 4)
|
||||||
|
if (p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant()) {
|
||||||
|
p.setStatut(Paris.StatutParis.REMBOURSE);
|
||||||
|
p.setGains(p.getMise());
|
||||||
|
parisRepository.save(p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des paris gagnants
|
||||||
|
if (estParisGagnant(p, resultat, aDeadHeat)) {
|
||||||
|
p.setStatut(Paris.StatutParis.GAGNANT);
|
||||||
|
double gains = calculerGains(p, resultat);
|
||||||
|
p.setGains(gains);
|
||||||
|
} else {
|
||||||
|
p.setStatut(Paris.StatutParis.PERDANT);
|
||||||
|
p.setGains(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
parisRepository.save(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estParisGagnant(Paris paris, ResultatCourse resultat, boolean aDeadHeat) {
|
||||||
|
if (aDeadHeat) {
|
||||||
|
return estParisGagnantAvecDeadHeat(paris, resultat);
|
||||||
|
} else {
|
||||||
|
// Cas normal (Article 1)
|
||||||
|
return paris.getPremier().equals(resultat.getPremier()) &&
|
||||||
|
paris.getDeuxieme().equals(resultat.getDeuxieme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estParisGagnantAvecDeadHeat(Paris paris, ResultatCourse resultat) {
|
||||||
|
// Implémentation pour les cas de dead heat (Article 3)
|
||||||
|
List<Long> chevauxDeadHeat = resultat.getChevauxDeadHeat();
|
||||||
|
|
||||||
|
if (chevauxDeadHeat.contains(paris.getPremier().getId())) {
|
||||||
|
// Dead heat à la première place
|
||||||
|
return chevauxDeadHeat.contains(paris.getDeuxieme().getId()) ||
|
||||||
|
paris.getDeuxieme().equals(resultat.getDeuxieme());
|
||||||
|
} else if (chevauxDeadHeat.contains(paris.getDeuxieme().getId())) {
|
||||||
|
// Dead heat à la deuxième place
|
||||||
|
return paris.getPremier().equals(resultat.getPremier());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculerGains(Paris paris, ResultatCourse resultat) {
|
||||||
|
// Calcul simplifié - implémenter la logique complète basée sur les Articles 5, 8, 9
|
||||||
|
double gainsBase = paris.getMise() * 5.0; // Exemple de ratio de gains
|
||||||
|
return Math.max(gainsBase, paris.getMise() * 1.1); // Minimum 1.1 par unité (Article 5d)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.pmumali.quarteplus.controller;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.*;
|
||||||
|
import com.pmumali.quarteplus.repository.PaiementRepository;
|
||||||
|
import com.pmumali.quarteplus.repository.PariQuartePlusRepository;
|
||||||
|
import com.pmumali.quarteplus.service.ServiceQuartePlus;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/quarte-plus")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ControleurQuartePlus {
|
||||||
|
|
||||||
|
private final ServiceQuartePlus service;
|
||||||
|
private final PariQuartePlusRepository pariRepository;
|
||||||
|
private final PaiementRepository paiementRepository;
|
||||||
|
|
||||||
|
@PostMapping("/pari")
|
||||||
|
public ResponseEntity<PariQuartePlus> placerPari(@RequestBody RequetePari requete) {
|
||||||
|
try {
|
||||||
|
PariQuartePlus pari = service.placerPari(requete);
|
||||||
|
return ResponseEntity.ok(pari);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/calculer-paiements")
|
||||||
|
public ResponseEntity<List<ReponsePaiement>> calculerPaiements(@RequestBody RequeteResultat requete) {
|
||||||
|
try {
|
||||||
|
List<ReponsePaiement> paiements = service.calculerPaiements(requete);
|
||||||
|
return ResponseEntity.ok(paiements);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dead-heat/{type}")
|
||||||
|
public ResponseEntity<List<ReponsePaiement>> gererDeadHeat(
|
||||||
|
@RequestBody RequeteResultat requete,
|
||||||
|
@PathVariable TypeDeadHeat type) {
|
||||||
|
try {
|
||||||
|
List<ReponsePaiement> paiements = service.gererDeadHeat(requete, type);
|
||||||
|
return ResponseEntity.ok(paiements);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/calcul-combinaison")
|
||||||
|
public ResponseEntity<CalculCombinaison> calculerCombinaison(
|
||||||
|
@RequestParam TypePari typePari,
|
||||||
|
@RequestParam Integer nombreChevaux) {
|
||||||
|
try {
|
||||||
|
CalculCombinaison resultat = service.calculerCombinaison(typePari, nombreChevaux);
|
||||||
|
return ResponseEntity.ok(resultat);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cagnotte")
|
||||||
|
public ResponseEntity<Cagnotte> creerCagnotte(@RequestParam Double montant) {
|
||||||
|
try {
|
||||||
|
Cagnotte cagnotte = service.creerCagnotte(montant);
|
||||||
|
return ResponseEntity.ok(cagnotte);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cagnotte/{cagnotteId}/utiliser")
|
||||||
|
public ResponseEntity<Void> utiliserCagnotte(
|
||||||
|
@PathVariable Long cagnotteId,
|
||||||
|
@RequestParam Long courseId) {
|
||||||
|
try {
|
||||||
|
service.utiliserCagnotte(cagnotteId, courseId);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/paris/course/{courseId}")
|
||||||
|
public ResponseEntity<List<PariQuartePlus>> getParisCourse(@PathVariable Long courseId) {
|
||||||
|
return ResponseEntity.ok(pariRepository.findByCourseId(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/paiements/pari/{pariId}")
|
||||||
|
public ResponseEntity<List<Paiement>> getPaiementsPari(@PathVariable Long pariId) {
|
||||||
|
return ResponseEntity.ok(paiementRepository.findByPariId(pariId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.pmumali.quarteplus.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GestionnaireExceptions {
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
|
||||||
|
return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<String> handleException(Exception ex) {
|
||||||
|
return ResponseEntity.internalServerError().body("Erreur interne: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/com/pmumali/quarteplus/model/Cagnotte.java
Normal file
23
src/main/java/com/pmumali/quarteplus/model/Cagnotte.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "cagnotte")
|
||||||
|
public class Cagnotte {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Double montant;
|
||||||
|
private LocalDateTime dateCreation;
|
||||||
|
private LocalDateTime dateUtilisation;
|
||||||
|
private Boolean utilisee;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class CalculCombinaison {
|
||||||
|
private TypePari typePari;
|
||||||
|
private Integer nombreChevaux;
|
||||||
|
private Integer nombreCombinaisons;
|
||||||
|
private Double valeurMise;
|
||||||
|
}
|
||||||
22
src/main/java/com/pmumali/quarteplus/model/Cheval.java
Normal file
22
src/main/java/com/pmumali/quarteplus/model/Cheval.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "cheval")
|
||||||
|
public class Cheval {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String nom;
|
||||||
|
private Integer numero;
|
||||||
|
private Boolean nonPartant;
|
||||||
|
}
|
||||||
28
src/main/java/com/pmumali/quarteplus/model/Course.java
Normal file
28
src/main/java/com/pmumali/quarteplus/model/Course.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "course")
|
||||||
|
public class Course {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String nom;
|
||||||
|
private LocalDateTime heureCourse;
|
||||||
|
|
||||||
|
@OneToMany
|
||||||
|
private List<Cheval> chevaux;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private StatutCourse statut;
|
||||||
|
}
|
||||||
27
src/main/java/com/pmumali/quarteplus/model/Paiement.java
Normal file
27
src/main/java/com/pmumali/quarteplus/model/Paiement.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "paiement")
|
||||||
|
public class Paiement {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private PariQuartePlus pari;
|
||||||
|
|
||||||
|
private Double montant;
|
||||||
|
private LocalDateTime heurePaiement;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private TypePaiement typePaiement;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "pari_quarte_plus")
|
||||||
|
public class PariQuartePlus {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@OrderColumn(name = "ordre_cheval")
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> chevauxSelectionnes;
|
||||||
|
|
||||||
|
private Double mise;
|
||||||
|
private LocalDateTime heurePari;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private TypePari typePari;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Parieur parieur;
|
||||||
|
|
||||||
|
private Boolean validationOrdreExact;
|
||||||
|
}
|
||||||
20
src/main/java/com/pmumali/quarteplus/model/Parieur.java
Normal file
20
src/main/java/com/pmumali/quarteplus/model/Parieur.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "parieur")
|
||||||
|
public class Parieur {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String nom;
|
||||||
|
private String identification;
|
||||||
|
private Double miseTotale;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ReponsePaiement {
|
||||||
|
private Long pariId;
|
||||||
|
private Double montant;
|
||||||
|
private TypePaiement typePaiement;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
13
src/main/java/com/pmumali/quarteplus/model/RequetePari.java
Normal file
13
src/main/java/com/pmumali/quarteplus/model/RequetePari.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RequetePari {
|
||||||
|
private Long courseId;
|
||||||
|
private List<Long> chevalIds;
|
||||||
|
private Double mise;
|
||||||
|
private TypePari typePari;
|
||||||
|
private Long parieurId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RequeteResultat {
|
||||||
|
private Long courseId;
|
||||||
|
private List<Long> ordreArriveeIds;
|
||||||
|
private List<Long> premiersIds;
|
||||||
|
private List<Long> secondsIds;
|
||||||
|
private List<Long> troisiemesIds;
|
||||||
|
private List<Long> quatriemesIds;
|
||||||
|
private Double recetteNette;
|
||||||
|
private Double prelevementsLegaux;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "resultat_course")
|
||||||
|
public class ResultatCourse {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@OrderColumn(name = "ordre_arrivee")
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> ordreArrivee;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> premiers;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> seconds;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> troisiemes;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
private List<Cheval> quatriemes;
|
||||||
|
|
||||||
|
private Double recetteNette;
|
||||||
|
private Double montantRembourse;
|
||||||
|
private Double prelevementsLegaux;
|
||||||
|
private Double masseAPartager;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
public enum StatutCourse {
|
||||||
|
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
|
||||||
|
}
|
||||||
12
src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java
Normal file
12
src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
public enum TypeDeadHeat {
|
||||||
|
QUATRE_PREMIERS_OU_PLUS,
|
||||||
|
TROIS_PREMIERS_UN_QUATRIEME,
|
||||||
|
DEUX_PREMIERS_DEUX_TROISIEMES,
|
||||||
|
DEUX_PREMIERS_UN_TROISIEME_UN_QUATRIEME,
|
||||||
|
TROIS_SECONDS_OU_PLUS,
|
||||||
|
DEUX_SECONDS_UN_QUATRIEME,
|
||||||
|
DEUX_TROISIEMES_OU_PLUS,
|
||||||
|
DEUX_QUATRIEMES_OU_PLUS
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
public enum TypePaiement {
|
||||||
|
QUARTE_PLUS_ORDRE_EXACT,
|
||||||
|
QUARTE_PLUS_ORDRE_INEXACT,
|
||||||
|
BONUS_3,
|
||||||
|
BONUS_3_BIS,
|
||||||
|
REMBOURSEMENT
|
||||||
|
}
|
||||||
6
src/main/java/com/pmumali/quarteplus/model/TypePari.java
Normal file
6
src/main/java/com/pmumali/quarteplus/model/TypePari.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package com.pmumali.quarteplus.model;
|
||||||
|
|
||||||
|
public enum TypePari {
|
||||||
|
UNITAIRE, COMBINE, CHAMP_TOTAL_3, CHAMP_PARTIEL_3,
|
||||||
|
CHAMP_TOTAL_2, CHAMP_PARTIEL_2, CHAMP_TOTAL_1, CHAMP_PARTIEL_1
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.Cagnotte;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CagnotteRepository extends JpaRepository<Cagnotte, Long> {
|
||||||
|
List<Cagnotte> findByUtilisee(Boolean utilisee);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.Cheval;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
|
||||||
|
List<Cheval> findByNonPartant(Boolean nonPartant);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.Course;
|
||||||
|
import com.pmumali.quarteplus.model.StatutCourse;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CourseRepository extends JpaRepository<Course, Long> {
|
||||||
|
List<Course> findByStatut(StatutCourse statut);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.Paiement;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PaiementRepository extends JpaRepository<Paiement, Long> {
|
||||||
|
List<Paiement> findByPariId(Long pariId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.PariQuartePlus;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PariQuartePlusRepository extends JpaRepository<PariQuartePlus, Long> {
|
||||||
|
List<PariQuartePlus> findByCourseId(Long courseId);
|
||||||
|
List<PariQuartePlus> findByParieurId(Long parieurId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.Parieur;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ParieurRepository extends JpaRepository<Parieur, Long> {
|
||||||
|
List<Parieur> findByNomContaining(String nom);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.pmumali.quarteplus.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.ResultatCourse;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
|
||||||
|
ResultatCourse findByCourseId(Long courseId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
package com.pmumali.quarteplus.service;
|
||||||
|
|
||||||
|
import com.pmumali.quarteplus.model.*;
|
||||||
|
import com.pmumali.quarteplus.repository.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServiceQuartePlus {
|
||||||
|
|
||||||
|
private final PariQuartePlusRepository pariRepository;
|
||||||
|
private final CourseRepository courseRepository;
|
||||||
|
private final ChevalRepository chevalRepository;
|
||||||
|
private final PaiementRepository paiementRepository;
|
||||||
|
private final ResultatCourseRepository resultatRepository;
|
||||||
|
private final CagnotteRepository cagnotteRepository;
|
||||||
|
private final ParieurRepository parieurRepository;
|
||||||
|
|
||||||
|
private static final Double MISE_BASE = 500.0;
|
||||||
|
private static final Double MISE_MAX = 20 * MISE_BASE;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PariQuartePlus placerPari(RequetePari requete) {
|
||||||
|
// Validation de la mise
|
||||||
|
if (requete.getMise() < MISE_BASE) {
|
||||||
|
throw new IllegalArgumentException("La mise doit être au moins " + MISE_BASE + " FCFA");
|
||||||
|
}
|
||||||
|
|
||||||
|
Course course = courseRepository.findById(requete.getCourseId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
|
||||||
|
|
||||||
|
List<Cheval> chevaux = chevalRepository.findAllById(requete.getChevalIds());
|
||||||
|
|
||||||
|
if (chevaux.size() != 4) {
|
||||||
|
throw new IllegalArgumentException("Exactement 4 chevaux doivent être sélectionnés");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des non-partants
|
||||||
|
long nonPartants = chevaux.stream().filter(Cheval::getNonPartant).count();
|
||||||
|
if (nonPartants >= 2) {
|
||||||
|
throw new IllegalArgumentException("Maximum 1 cheval non-partant autorisé");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limitation de mise selon l'article 2
|
||||||
|
Double miseEffective = Math.min(requete.getMise(), MISE_MAX);
|
||||||
|
|
||||||
|
Parieur parieur = parieurRepository.findById(requete.getParieurId()).orElseThrow(() -> new RuntimeException("Parieur non trouvé"));
|
||||||
|
|
||||||
|
PariQuartePlus pari = PariQuartePlus.builder()
|
||||||
|
.course(course)
|
||||||
|
.chevauxSelectionnes(chevaux)
|
||||||
|
.mise(miseEffective)
|
||||||
|
.heurePari(LocalDateTime.now())
|
||||||
|
.typePari(requete.getTypePari())
|
||||||
|
.parieur(parieur)
|
||||||
|
.validationOrdreExact(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return pariRepository.save(pari);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<ReponsePaiement> calculerPaiements(RequeteResultat requete) {
|
||||||
|
ResultatCourse resultat = creerResultat(requete);
|
||||||
|
List<PariQuartePlus> paris = pariRepository.findByCourseId(requete.getCourseId());
|
||||||
|
List<ReponsePaiement> paiements = new ArrayList<>();
|
||||||
|
|
||||||
|
// Calcul de la masse à partager selon l'article 5
|
||||||
|
Double masseAPartager = calculerMasseAPartager(resultat);
|
||||||
|
resultat.setMasseAPartager(masseAPartager);
|
||||||
|
resultatRepository.save(resultat);
|
||||||
|
|
||||||
|
// Compter les paris gagnants par type
|
||||||
|
Map<TypePaiement, Integer> compteurs = new HashMap<>();
|
||||||
|
Map<TypePaiement, Double> montantsTotaux = new HashMap<>();
|
||||||
|
|
||||||
|
for (PariQuartePlus pari : paris) {
|
||||||
|
ReponsePaiement paiement = calculerPaiementPari(pari, resultat, masseAPartager);
|
||||||
|
if (paiement != null) {
|
||||||
|
compteurs.put(paiement.getTypePaiement(),
|
||||||
|
compteurs.getOrDefault(paiement.getTypePaiement(), 0) + 1);
|
||||||
|
montantsTotaux.put(paiement.getTypePaiement(),
|
||||||
|
montantsTotaux.getOrDefault(paiement.getTypePaiement(), 0.0) + paiement.getMontant());
|
||||||
|
paiements.add(paiement);
|
||||||
|
enregistrerPaiement(pari, paiement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajuster les rapports selon les proportions minimales (Article 6)
|
||||||
|
ajusterProportionsMinimales(paiements, compteurs, montantsTotaux, masseAPartager);
|
||||||
|
|
||||||
|
return paiements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement calculerPaiementPari(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
|
||||||
|
long nonPartants = chevauxPari.stream().filter(Cheval::getNonPartant).count();
|
||||||
|
|
||||||
|
// Article 4: Gestion des non-partants
|
||||||
|
if (nonPartants >= 2) {
|
||||||
|
return new ReponsePaiement(pari.getId(), pari.getMise(),
|
||||||
|
TypePaiement.REMBOURSEMENT, "Remboursement - 2+ non-partants");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'ordre exact
|
||||||
|
boolean ordreExact = estOrdreExact(chevauxPari, resultat.getOrdreArrivee());
|
||||||
|
|
||||||
|
// Vérifier les positions
|
||||||
|
boolean tousEnTop4 = chevauxPari.stream().allMatch(cheval ->
|
||||||
|
estDansListe(cheval, resultat.getPremiers()) ||
|
||||||
|
estDansListe(cheval, resultat.getSeconds()) ||
|
||||||
|
estDansListe(cheval, resultat.getTroisiemes()) ||
|
||||||
|
estDansListe(cheval, resultat.getQuatriemes()));
|
||||||
|
|
||||||
|
if (ordreExact && tousEnTop4) {
|
||||||
|
Double montant = calculerMontantQuartePlusOrdreExact(masseAPartager);
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Quarte Plus ordre exact");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tousEnTop4) {
|
||||||
|
Double montant = calculerMontantQuartePlusOrdreInexact(masseAPartager);
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, "Quarte Plus ordre inexact");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier Bonus 3 avec non-partant (Bonus 3 Bis)
|
||||||
|
if (nonPartants == 1 && estEligibleBonus3(pari, resultat)) {
|
||||||
|
Double montant = calculerMontantBonus3(masseAPartager) * 3; // Triple selon article 4b
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.BONUS_3_BIS, "Bonus 3 Bis avec non-partant");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier Bonus 3 normal
|
||||||
|
if (estEligibleBonus3(pari, resultat)) {
|
||||||
|
Double montant = calculerMontantBonus3(masseAPartager);
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.BONUS_3, "Bonus 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estOrdreExact(List<Cheval> chevauxPari, List<Cheval> ordreArrivee) {
|
||||||
|
if (chevauxPari.size() != 4 || ordreArrivee.size() < 4) return false;
|
||||||
|
|
||||||
|
return chevauxPari.get(0).equals(ordreArrivee.get(0)) &&
|
||||||
|
chevauxPari.get(1).equals(ordreArrivee.get(1)) &&
|
||||||
|
chevauxPari.get(2).equals(ordreArrivee.get(2)) &&
|
||||||
|
chevauxPari.get(3).equals(ordreArrivee.get(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estDansListe(Cheval cheval, List<Cheval> liste) {
|
||||||
|
return liste != null && liste.contains(cheval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estEligibleBonus3(PariQuartePlus pari, ResultatCourse resultat) {
|
||||||
|
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
|
||||||
|
|
||||||
|
if (chevauxPari.size() < 3 || resultat.getOrdreArrivee().size() < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si les 3 premiers du pari correspondent aux 3 premiers à l'arrivée
|
||||||
|
List<Cheval> troisPremiersArrivee = resultat.getOrdreArrivee().subList(0, 3);
|
||||||
|
List<Cheval> troisPremiersPari = chevauxPari.subList(0, 3);
|
||||||
|
|
||||||
|
if (!troisPremiersPari.equals(troisPremiersArrivee)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le 4ème cheval n'est pas dans les 4 premiers (pour Bonus 3 normal)
|
||||||
|
Cheval quatriemePari = chevauxPari.get(3);
|
||||||
|
boolean estDansTop4 = estDansListe(quatriemePari, resultat.getPremiers()) ||
|
||||||
|
estDansListe(quatriemePari, resultat.getSeconds()) ||
|
||||||
|
estDansListe(quatriemePari, resultat.getTroisiemes()) ||
|
||||||
|
estDansListe(quatriemePari, resultat.getQuatriemes());
|
||||||
|
|
||||||
|
// Pour Bonus 3 normal, le 4ème ne doit pas être dans le top 4
|
||||||
|
// Pour Bonus 3 Bis (avec non-partant), on a déjà géré ça séparément
|
||||||
|
return !estDansTop4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double calculerMasseAPartager(ResultatCourse resultat) {
|
||||||
|
// Article 5: MAP = RNET - MREMB - PRELEV
|
||||||
|
return resultat.getRecetteNette() -
|
||||||
|
resultat.getMontantRembourse() -
|
||||||
|
resultat.getPrelevementsLegaux();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double calculerMontantQuartePlusOrdreExact(Double masseAPartager) {
|
||||||
|
// Article 5: 40% de la masse pour l'ordre exact
|
||||||
|
return masseAPartager * 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double calculerMontantQuartePlusOrdreInexact(Double masseAPartager) {
|
||||||
|
// Article 5: 35% de la masse pour l'ordre inexact
|
||||||
|
return masseAPartager * 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double calculerMontantBonus3(Double masseAPartager) {
|
||||||
|
// Article 5: 25% de la masse pour le Bonus 3
|
||||||
|
return masseAPartager * 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enregistrerPaiement(PariQuartePlus pari, ReponsePaiement reponse) {
|
||||||
|
Paiement paiement = Paiement.builder()
|
||||||
|
.pari(pari)
|
||||||
|
.montant(reponse.getMontant())
|
||||||
|
.heurePaiement(LocalDateTime.now())
|
||||||
|
.typePaiement(reponse.getTypePaiement())
|
||||||
|
.build();
|
||||||
|
paiementRepository.save(paiement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultatCourse creerResultat(RequeteResultat requete) {
|
||||||
|
Course course = courseRepository.findById(requete.getCourseId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
|
||||||
|
|
||||||
|
return ResultatCourse.builder()
|
||||||
|
.course(course)
|
||||||
|
.ordreArrivee(chevalRepository.findAllById(requete.getOrdreArriveeIds()))
|
||||||
|
.premiers(chevalRepository.findAllById(requete.getPremiersIds()))
|
||||||
|
.seconds(chevalRepository.findAllById(requete.getSecondsIds()))
|
||||||
|
.troisiemes(chevalRepository.findAllById(requete.getTroisiemesIds()))
|
||||||
|
.quatriemes(chevalRepository.findAllById(requete.getQuatriemesIds()))
|
||||||
|
.recetteNette(requete.getRecetteNette())
|
||||||
|
.prelevementsLegaux(requete.getPrelevementsLegaux())
|
||||||
|
.montantRembourse(0.0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ajusterProportionsMinimales(List<ReponsePaiement> paiements,
|
||||||
|
Map<TypePaiement, Integer> compteurs,
|
||||||
|
Map<TypePaiement, Double> montantsTotaux,
|
||||||
|
Double masseAPartager) {
|
||||||
|
// Article 6: Ajustement des proportions minimales
|
||||||
|
|
||||||
|
// Vérifier ratio ordre exact/inexact
|
||||||
|
Double montantOrdreExact = montantsTotaux.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_EXACT, 0.0);
|
||||||
|
Double montantOrdreInexact = montantsTotaux.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0.0);
|
||||||
|
|
||||||
|
if (montantOrdreExact > 0 && montantOrdreInexact > 0) {
|
||||||
|
double ratio = montantOrdreExact / montantOrdreInexact;
|
||||||
|
|
||||||
|
// Article 6a: Ratio minimal de 8 pour 1
|
||||||
|
if (ratio < 8.0) {
|
||||||
|
redistribuerMasse(paiements, compteurs, masseAPartager, 8.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier ratio Bonus 3/Quarte inexact (Article 7)
|
||||||
|
Double montantBonus3 = montantsTotaux.getOrDefault(TypePaiement.BONUS_3, 0.0);
|
||||||
|
if (montantBonus3 > 0 && montantOrdreInexact > 0) {
|
||||||
|
double ratio = montantBonus3 / montantOrdreInexact;
|
||||||
|
|
||||||
|
// Article 7: Bonus 3 ne peut dépasser 1/4 du Quarte inexact
|
||||||
|
if (ratio > 0.25) {
|
||||||
|
ajusterRatioBonus3(paiements, compteurs, montantsTotaux, masseAPartager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redistribuerMasse(List<ReponsePaiement> paiements,
|
||||||
|
Map<TypePaiement, Integer> compteurs,
|
||||||
|
Double masseAPartager, Double coefficient) {
|
||||||
|
// Implémentation de la redistribution selon l'article 6
|
||||||
|
int countOrdreExact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_EXACT, 0);
|
||||||
|
int countOrdreInexact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0);
|
||||||
|
|
||||||
|
if (countOrdreExact > 0 && countOrdreInexact > 0) {
|
||||||
|
double totalMisesPonderees = (countOrdreExact * coefficient) + countOrdreInexact;
|
||||||
|
double rapportBase = masseAPartager / totalMisesPonderees;
|
||||||
|
|
||||||
|
// Mettre à jour les montants
|
||||||
|
for (ReponsePaiement paiement : paiements) {
|
||||||
|
if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_EXACT) {
|
||||||
|
paiement.setMontant(rapportBase * coefficient);
|
||||||
|
} else if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_INEXACT) {
|
||||||
|
paiement.setMontant(rapportBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ajusterRatioBonus3(List<ReponsePaiement> paiements,
|
||||||
|
Map<TypePaiement, Integer> compteurs,
|
||||||
|
Map<TypePaiement, Double> montantsTotaux,
|
||||||
|
Double masseAPartager) {
|
||||||
|
// Article 7: Ajustement du ratio Bonus 3
|
||||||
|
int countOrdreInexact = compteurs.getOrDefault(TypePaiement.QUARTE_PLUS_ORDRE_INEXACT, 0);
|
||||||
|
int countBonus3 = compteurs.getOrDefault(TypePaiement.BONUS_3, 0);
|
||||||
|
|
||||||
|
if (countOrdreInexact > 0 && countBonus3 > 0) {
|
||||||
|
double totalMisesPonderees = (countOrdreInexact * 4) + countBonus3;
|
||||||
|
double rapportBase = masseAPartager * 0.35 / totalMisesPonderees; // 35% pour l'ordre inexact
|
||||||
|
|
||||||
|
// Mettre à jour les montants
|
||||||
|
for (ReponsePaiement paiement : paiements) {
|
||||||
|
if (paiement.getTypePaiement() == TypePaiement.QUARTE_PLUS_ORDRE_INEXACT) {
|
||||||
|
paiement.setMontant(rapportBase * 4);
|
||||||
|
} else if (paiement.getTypePaiement() == TypePaiement.BONUS_3) {
|
||||||
|
paiement.setMontant(rapportBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour les calculs de combinaisons (Article 8)
|
||||||
|
public CalculCombinaison calculerCombinaison(TypePari typePari, Integer nombreChevaux) {
|
||||||
|
Integer nombreCombinaisons = 0;
|
||||||
|
|
||||||
|
switch (typePari) {
|
||||||
|
case UNITAIRE:
|
||||||
|
nombreCombinaisons = 1;
|
||||||
|
break;
|
||||||
|
case COMBINE:
|
||||||
|
nombreCombinaisons = (nombreChevaux * (nombreChevaux - 1) *
|
||||||
|
(nombreChevaux - 2) * (nombreChevaux - 3)) / 24;
|
||||||
|
break;
|
||||||
|
case CHAMP_TOTAL_3:
|
||||||
|
nombreCombinaisons = 24 * (nombreChevaux - 3);
|
||||||
|
break;
|
||||||
|
case CHAMP_PARTIEL_3:
|
||||||
|
nombreCombinaisons = 24 * nombreChevaux;
|
||||||
|
break;
|
||||||
|
case CHAMP_TOTAL_2:
|
||||||
|
nombreCombinaisons = 12 * (nombreChevaux - 2) * (nombreChevaux - 3);
|
||||||
|
break;
|
||||||
|
case CHAMP_PARTIEL_2:
|
||||||
|
nombreCombinaisons = 12 * nombreChevaux * (nombreChevaux - 1);
|
||||||
|
break;
|
||||||
|
case CHAMP_TOTAL_1:
|
||||||
|
nombreCombinaisons = 4 * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3);
|
||||||
|
break;
|
||||||
|
case CHAMP_PARTIEL_1:
|
||||||
|
nombreCombinaisons = 4 * nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Double valeurMise = nombreCombinaisons * MISE_BASE;
|
||||||
|
|
||||||
|
return CalculCombinaison.builder()
|
||||||
|
.typePari(typePari)
|
||||||
|
.nombreChevaux(nombreChevaux)
|
||||||
|
.nombreCombinaisons(nombreCombinaisons)
|
||||||
|
.valeurMise(valeurMise)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour gérer les dead-heat (Article 3)
|
||||||
|
@Transactional
|
||||||
|
public List<ReponsePaiement> gererDeadHeat(RequeteResultat requete, TypeDeadHeat typeDeadHeat) {
|
||||||
|
ResultatCourse resultat = creerResultat(requete);
|
||||||
|
List<PariQuartePlus> paris = pariRepository.findByCourseId(requete.getCourseId());
|
||||||
|
List<ReponsePaiement> paiements = new ArrayList<>();
|
||||||
|
|
||||||
|
Double masseAPartager = calculerMasseAPartager(resultat);
|
||||||
|
|
||||||
|
for (PariQuartePlus pari : paris) {
|
||||||
|
ReponsePaiement paiement = calculerPaiementDeadHeat(pari, resultat, masseAPartager, typeDeadHeat);
|
||||||
|
if (paiement != null) {
|
||||||
|
paiements.add(paiement);
|
||||||
|
enregistrerPaiement(pari, paiement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paiements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement calculerPaiementDeadHeat(PariQuartePlus pari, ResultatCourse resultat,
|
||||||
|
Double masseAPartager, TypeDeadHeat typeDeadHeat) {
|
||||||
|
switch (typeDeadHeat) {
|
||||||
|
case QUATRE_PREMIERS_OU_PLUS:
|
||||||
|
return gererDeadHeatQuatrePremiers(pari, resultat, masseAPartager);
|
||||||
|
case TROIS_PREMIERS_UN_QUATRIEME:
|
||||||
|
return gererDeadHeatTroisPremiersUnQuatrieme(pari, resultat, masseAPartager);
|
||||||
|
case DEUX_PREMIERS_DEUX_TROISIEMES:
|
||||||
|
return gererDeadHeatDeuxPremiersDeuxTroisiemes(pari, resultat, masseAPartager);
|
||||||
|
case DEUX_PREMIERS_UN_TROISIEME_UN_QUATRIEME:
|
||||||
|
return gererDeadHeatDeuxPremiersUnTroisiemeUnQuatrieme(pari, resultat, masseAPartager);
|
||||||
|
case TROIS_SECONDS_OU_PLUS:
|
||||||
|
return gererDeadHeatTroisSeconds(pari, resultat, masseAPartager);
|
||||||
|
case DEUX_SECONDS_UN_QUATRIEME:
|
||||||
|
return gererDeadHeatDeuxSecondsUnQuatrieme(pari, resultat, masseAPartager);
|
||||||
|
case DEUX_TROISIEMES_OU_PLUS:
|
||||||
|
return gererDeadHeatDeuxTroisiemes(pari, resultat, masseAPartager);
|
||||||
|
case DEUX_QUATRIEMES_OU_PLUS:
|
||||||
|
return gererDeadHeatDeuxQuatriemes(pari, resultat, masseAPartager);
|
||||||
|
default:
|
||||||
|
return calculerPaiementPari(pari, resultat, masseAPartager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatQuatrePremiers(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3a: Dead-heat de 4+ chevaux premiers
|
||||||
|
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
|
||||||
|
|
||||||
|
if (chevauxPari.stream().allMatch(cheval -> estDansListe(cheval, resultat.getPremiers()))) {
|
||||||
|
int nombreCombinaisons = compterCombinaisonsDeadHeat(resultat.getPremiers().size(), 4);
|
||||||
|
Double montant = masseAPartager / nombreCombinaisons;
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat 4+ premiers");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatTroisPremiersUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3b: Dead-heat de 3 premiers + 1+ quatrième
|
||||||
|
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
|
||||||
|
|
||||||
|
boolean troisPremiersOk = chevauxPari.subList(0, 3).stream()
|
||||||
|
.allMatch(cheval -> estDansListe(cheval, resultat.getPremiers()));
|
||||||
|
boolean quatriemeOk = estDansListe(chevauxPari.get(3), resultat.getQuatriemes());
|
||||||
|
|
||||||
|
if (troisPremiersOk && quatriemeOk) {
|
||||||
|
// Calcul spécifique avec 6 permutations ordre exact, 18 ordre inexact
|
||||||
|
Double montant = calculerMontantDeadHeat35(masseAPartager, true);
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat 3 premiers + 1 quatrième");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implémentations similaires pour les autres types de dead-heat...
|
||||||
|
private ReponsePaiement gererDeadHeatDeuxPremiersDeuxTroisiemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3c: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatDeuxPremiersUnTroisiemeUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3d: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatTroisSeconds(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3e: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatDeuxSecondsUnQuatrieme(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3f: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatDeuxTroisiemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3g: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererDeadHeatDeuxQuatriemes(PariQuartePlus pari, ResultatCourse resultat, Double masseAPartager) {
|
||||||
|
// Article 3h: Implémentation spécifique
|
||||||
|
return calculerPaiementGeneriqueDeadHeat(pari, resultat, masseAPartager, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement calculerPaiementGeneriqueDeadHeat(PariQuartePlus pari, ResultatCourse resultat,
|
||||||
|
Double masseAPartager, Double coefficient) {
|
||||||
|
// Méthode générique pour les dead-heat avec coefficient spécifique
|
||||||
|
List<Cheval> chevauxPari = pari.getChevauxSelectionnes();
|
||||||
|
|
||||||
|
if (chevauxPari.stream().allMatch(cheval ->
|
||||||
|
estDansListe(cheval, resultat.getPremiers()) ||
|
||||||
|
estDansListe(cheval, resultat.getSeconds()) ||
|
||||||
|
estDansListe(cheval, resultat.getTroisiemes()) ||
|
||||||
|
estDansListe(cheval, resultat.getQuatriemes()))) {
|
||||||
|
|
||||||
|
int nombreCombinaisons = compterCombinaisonsGagnantesDeadHeat(resultat);
|
||||||
|
Double montant = (masseAPartager * 0.4) / (nombreCombinaisons * coefficient);
|
||||||
|
return new ReponsePaiement(pari.getId(), montant,
|
||||||
|
TypePaiement.QUARTE_PLUS_ORDRE_EXACT, "Dead-Heat générique");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compterCombinaisonsDeadHeat(int nombreChevaux, int prise) {
|
||||||
|
// Calcul des combinaisons C(n, k)
|
||||||
|
if (prise == 0) return 1;
|
||||||
|
if (nombreChevaux < prise) return 0;
|
||||||
|
|
||||||
|
int resultat = 1;
|
||||||
|
for (int i = 1; i <= prise; i++) {
|
||||||
|
resultat = resultat * (nombreChevaux - i + 1) / i;
|
||||||
|
}
|
||||||
|
return resultat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compterCombinaisonsGagnantesDeadHeat(ResultatCourse resultat) {
|
||||||
|
// Comptage simplifié des combinaisons gagnantes
|
||||||
|
// Implémentation réelle dépendrait de la structure exacte du dead-heat
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double calculerMontantDeadHeat35(Double masseAPartager, boolean ordreExact) {
|
||||||
|
// Calcul spécifique pour dead-heat 3+1 avec ratio 6:18
|
||||||
|
double portion = ordreExact ? 0.4 : 0.35;
|
||||||
|
double ratio = ordreExact ? 6.0/24.0 : 18.0/24.0;
|
||||||
|
return masseAPartager * portion * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion de la cagnotte (Article 10)
|
||||||
|
@Transactional
|
||||||
|
public Cagnotte creerCagnotte(Double montant) {
|
||||||
|
Cagnotte cagnotte = Cagnotte.builder()
|
||||||
|
.montant(montant)
|
||||||
|
.dateCreation(LocalDateTime.now())
|
||||||
|
.utilisee(false)
|
||||||
|
.build();
|
||||||
|
return cagnotteRepository.save(cagnotte);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void utiliserCagnotte(Long cagnotteId, Long courseId) {
|
||||||
|
Cagnotte cagnotte = cagnotteRepository.findById(cagnotteId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cagnotte non trouvée"));
|
||||||
|
|
||||||
|
if (cagnotte.getUtilisee()) {
|
||||||
|
throw new RuntimeException("Cagnotte déjà utilisée");
|
||||||
|
}
|
||||||
|
|
||||||
|
cagnotte.setUtilisee(true);
|
||||||
|
cagnotte.setDateUtilisation(LocalDateTime.now());
|
||||||
|
cagnotteRepository.save(cagnotte);
|
||||||
|
|
||||||
|
// Ajouter le montant à la masse à partager de la course
|
||||||
|
ResultatCourse resultat = resultatRepository.findByCourseId(courseId);
|
||||||
|
if (resultat != null) {
|
||||||
|
resultat.setMasseAPartager(resultat.getMasseAPartager() + cagnotte.getMontant());
|
||||||
|
resultatRepository.save(resultat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<Cagnotte> getCagnottesDisponibles() {
|
||||||
|
return cagnotteRepository.findByUtilisee(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires supplémentaires
|
||||||
|
public Map<TypePari, Integer> getStatistiquesParis(Long courseId) {
|
||||||
|
List<PariQuartePlus> paris = pariRepository.findByCourseId(courseId);
|
||||||
|
return paris.stream()
|
||||||
|
.collect(Collectors.groupingBy(PariQuartePlus::getTypePari,
|
||||||
|
Collectors.summingInt(p -> 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getMasseTotaleCourse(Long courseId) {
|
||||||
|
ResultatCourse resultat = resultatRepository.findByCourseId(courseId);
|
||||||
|
return resultat != null ? resultat.getMasseAPartager() : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Paiement> getHistoriquePaiementsParieur(Long parieurId) {
|
||||||
|
List<PariQuartePlus> paris = pariRepository.findByParieurId(parieurId);
|
||||||
|
return paris.stream()
|
||||||
|
.flatMap(pari -> paiementRepository.findByPariId(pari.getId()).stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.pmumali.quatro.controller;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.*;
|
||||||
|
import com.pmumali.quatro.repository.*;
|
||||||
|
import com.pmumali.quatro.service.ServiceQuatro;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/quatro")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ControllerQuatro {
|
||||||
|
|
||||||
|
private final ServiceQuatro serviceQuatro;
|
||||||
|
private final PariRepository pariRepository;
|
||||||
|
private final PaiementRepository paiementRepository;
|
||||||
|
|
||||||
|
@PostMapping("/pari")
|
||||||
|
public ResponseEntity<Pari> placerPari(@RequestBody RequetePari requetePari) {
|
||||||
|
try {
|
||||||
|
Pari pari = serviceQuatro.placerPari(requetePari);
|
||||||
|
return ResponseEntity.ok(pari);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().body(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/calculer-paiements")
|
||||||
|
public ResponseEntity<List<ReponsePaiement>> calculerPaiements(@RequestBody RequeteResultatCourse requeteResultat) {
|
||||||
|
try {
|
||||||
|
List<ReponsePaiement> paiements = serviceQuatro.calculerPaiements(requeteResultat);
|
||||||
|
return ResponseEntity.ok(paiements);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().body(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/gerer-dead-heat")
|
||||||
|
public ResponseEntity<List<ReponsePaiement>> gererDeadHeat(@RequestBody RequeteResultatCourse requeteResultat) {
|
||||||
|
try {
|
||||||
|
List<ReponsePaiement> paiements = serviceQuatro.gererDeadHeat(requeteResultat);
|
||||||
|
return ResponseEntity.ok(paiements);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().body(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/paris/course/{courseId}")
|
||||||
|
public ResponseEntity<List<Pari>> getParisParCourse(@PathVariable Long courseId) {
|
||||||
|
return ResponseEntity.ok(pariRepository.findByCourseId(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/paiements/pari/{pariId}")
|
||||||
|
public ResponseEntity<List<Paiement>> getPaiementsParPari(@PathVariable Long pariId) {
|
||||||
|
return ResponseEntity.ok(paiementRepository.findByPariId(pariId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/calcul-combinaisons")
|
||||||
|
public ResponseEntity<Integer> calculerCombinaisons(
|
||||||
|
@RequestParam TypePari typePari,
|
||||||
|
@RequestParam int nombreChevaux) {
|
||||||
|
int nombreCombinaisons = serviceQuatro.calculerNombreCombinaisons(typePari, nombreChevaux);
|
||||||
|
return ResponseEntity.ok(nombreCombinaisons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/calcul-valeur-mise")
|
||||||
|
public ResponseEntity<Double> calculerValeurMise(
|
||||||
|
@RequestParam TypePari typePari,
|
||||||
|
@RequestParam int nombreChevaux) {
|
||||||
|
double valeurMise = serviceQuatro.calculerValeurMise(typePari, nombreChevaux);
|
||||||
|
return ResponseEntity.ok(valeurMise);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.pmumali.quatro.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GestionnaireExceptionsGlobal {
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
|
||||||
|
return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<String> handleExceptionGenerale(Exception ex) {
|
||||||
|
return ResponseEntity.internalServerError().body("Une erreur s'est produite: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/com/pmumali/quatro/model/Cheval.java
Normal file
29
src/main/java/com/pmumali/quatro/model/Cheval.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "cheval")
|
||||||
|
public class Cheval {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "nom")
|
||||||
|
private String nom;
|
||||||
|
|
||||||
|
@Column(name = "numero")
|
||||||
|
private Integer numero;
|
||||||
|
|
||||||
|
@Column(name = "non_partant")
|
||||||
|
private boolean nonPartant;
|
||||||
|
}
|
||||||
35
src/main/java/com/pmumali/quatro/model/Course.java
Normal file
35
src/main/java/com/pmumali/quatro/model/Course.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "course")
|
||||||
|
public class Course {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "nom")
|
||||||
|
private String nom;
|
||||||
|
|
||||||
|
@Column(name = "heure_course")
|
||||||
|
private LocalDateTime heureCourse;
|
||||||
|
|
||||||
|
@OneToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "course_chevaux",
|
||||||
|
joinColumns = @JoinColumn(name = "course_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
private List<Cheval> chevaux;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "statut")
|
||||||
|
private StatutCourse statut;
|
||||||
|
}
|
||||||
32
src/main/java/com/pmumali/quatro/model/Paiement.java
Normal file
32
src/main/java/com/pmumali/quatro/model/Paiement.java
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "paiement")
|
||||||
|
public class Paiement {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "pari_id")
|
||||||
|
private Pari pari;
|
||||||
|
|
||||||
|
@Column(name = "montant")
|
||||||
|
private Double montant;
|
||||||
|
|
||||||
|
@Column(name = "heure_paiement")
|
||||||
|
private LocalDateTime heurePaiement;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "type_paiement")
|
||||||
|
private TypePaiement typePaiement;
|
||||||
|
}
|
||||||
43
src/main/java/com/pmumali/quatro/model/Pari.java
Normal file
43
src/main/java/com/pmumali/quatro/model/Pari.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "pari")
|
||||||
|
public class Pari {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "course_id")
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "pari_chevaux",
|
||||||
|
joinColumns = @JoinColumn(name = "pari_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
private List<Cheval> chevauxSelectionnes;
|
||||||
|
|
||||||
|
@Column(name = "mise")
|
||||||
|
private Double mise;
|
||||||
|
|
||||||
|
@Column(name = "heure_pari")
|
||||||
|
private LocalDateTime heurePari;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "type_pari")
|
||||||
|
private TypePari typePari;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "parieur_id")
|
||||||
|
private Parieur parieur;
|
||||||
|
}
|
||||||
25
src/main/java/com/pmumali/quatro/model/Parieur.java
Normal file
25
src/main/java/com/pmumali/quatro/model/Parieur.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "parieur")
|
||||||
|
public class Parieur {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "nom")
|
||||||
|
private String nom;
|
||||||
|
|
||||||
|
@Column(name = "identification")
|
||||||
|
private String identification;
|
||||||
|
|
||||||
|
@Column(name = "mise_totale")
|
||||||
|
private Double miseTotale;
|
||||||
|
}
|
||||||
5
src/main/java/com/pmumali/quatro/model/Position.java
Normal file
5
src/main/java/com/pmumali/quatro/model/Position.java
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
public enum Position {
|
||||||
|
PREMIER, SECOND, TROISIEME, QUATRIEME
|
||||||
|
}
|
||||||
13
src/main/java/com/pmumali/quatro/model/ReponsePaiement.java
Normal file
13
src/main/java/com/pmumali/quatro/model/ReponsePaiement.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ReponsePaiement {
|
||||||
|
private Long pariId;
|
||||||
|
private Double montant;
|
||||||
|
private TypePaiement typePaiement;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
13
src/main/java/com/pmumali/quatro/model/RequetePari.java
Normal file
13
src/main/java/com/pmumali/quatro/model/RequetePari.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RequetePari {
|
||||||
|
private Long courseId;
|
||||||
|
private List<Long> chevalIds;
|
||||||
|
private Double mise;
|
||||||
|
private TypePari typePari;
|
||||||
|
private Long parieurId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RequeteResultatCourse {
|
||||||
|
private Long courseId;
|
||||||
|
private List<Long> chevauxPremiers;
|
||||||
|
private List<Long> chevauxSeconds;
|
||||||
|
private List<Long> chevauxTroisiemes;
|
||||||
|
private List<Long> chevauxQuatriemes;
|
||||||
|
private Double recetteNette;
|
||||||
|
private Double prelevementsLegaux;
|
||||||
|
}
|
||||||
59
src/main/java/com/pmumali/quatro/model/ResultatCourse.java
Normal file
59
src/main/java/com/pmumali/quatro/model/ResultatCourse.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Table(name = "resultat_course")
|
||||||
|
public class ResultatCourse {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "course_id")
|
||||||
|
private Course course;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "resultat_premiers",
|
||||||
|
joinColumns = @JoinColumn(name = "resultat_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
@OrderColumn(name = "ordre_position")
|
||||||
|
private List<Cheval> premiers;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "resultat_seconds",
|
||||||
|
joinColumns = @JoinColumn(name = "resultat_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
private List<Cheval> seconds;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "resultat_troisiemes",
|
||||||
|
joinColumns = @JoinColumn(name = "resultat_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
private List<Cheval> troisiemes;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "resultat_quatriemes",
|
||||||
|
joinColumns = @JoinColumn(name = "resultat_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "cheval_id"))
|
||||||
|
private List<Cheval> quatriemes;
|
||||||
|
|
||||||
|
@Column(name = "masse_partager")
|
||||||
|
private Double massePartager;
|
||||||
|
|
||||||
|
@Column(name = "recette_nette")
|
||||||
|
private Double recetteNette;
|
||||||
|
|
||||||
|
@Column(name = "montant_rembourse")
|
||||||
|
private Double montantRembourse;
|
||||||
|
|
||||||
|
@Column(name = "prelevements_legaux")
|
||||||
|
private Double prelevementsLegaux;
|
||||||
|
}
|
||||||
5
src/main/java/com/pmumali/quatro/model/StatutCourse.java
Normal file
5
src/main/java/com/pmumali/quatro/model/StatutCourse.java
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
public enum StatutCourse {
|
||||||
|
PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE
|
||||||
|
}
|
||||||
5
src/main/java/com/pmumali/quatro/model/TypePaiement.java
Normal file
5
src/main/java/com/pmumali/quatro/model/TypePaiement.java
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
public enum TypePaiement {
|
||||||
|
QUATRO, SPECIAL_TRIO, SPECIAL_JUMELE, SPECIAL_GAGNANT, REMBOURSEMENT
|
||||||
|
}
|
||||||
6
src/main/java/com/pmumali/quatro/model/TypePari.java
Normal file
6
src/main/java/com/pmumali/quatro/model/TypePari.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package com.pmumali.quatro.model;
|
||||||
|
|
||||||
|
public enum TypePari {
|
||||||
|
UNITAIRE, COMBINE, CHAMP_TOTAL_3, CHAMP_PARTIEL_3,
|
||||||
|
CHAMP_TOTAL_2, CHAMP_PARTIEL_2, CHAMP_TOTAL_1, CHAMP_PARTIEL_1
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.Cheval;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ChevalRepository extends JpaRepository<Cheval, Long> {
|
||||||
|
List<Cheval> findByNonPartant(boolean nonPartant);
|
||||||
|
List<Cheval> findByIdIn(List<Long> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.Course;
|
||||||
|
import com.pmumali.quatro.model.StatutCourse;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CourseRepository extends JpaRepository<Course, Long> {
|
||||||
|
List<Course> findByStatut(StatutCourse statut);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.Paiement;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PaiementRepository extends JpaRepository<Paiement, Long> {
|
||||||
|
List<Paiement> findByPariId(Long pariId);
|
||||||
|
List<Paiement> findByPariCourseId(Long courseId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.Pari;
|
||||||
|
import com.pmumali.quatro.model.TypePari;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PariRepository extends JpaRepository<Pari, Long> {
|
||||||
|
List<Pari> findByCourseId(Long courseId);
|
||||||
|
List<Pari> findByParieurId(Long parieurId);
|
||||||
|
List<Pari> findByCourseIdAndTypePari(Long courseId, TypePari typePari);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.Parieur;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ParieurRepository extends JpaRepository<Parieur, Long> {
|
||||||
|
List<Parieur> findByNomContaining(String nom);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.pmumali.quatro.repository;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.ResultatCourse;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ResultatCourseRepository extends JpaRepository<ResultatCourse, Long> {
|
||||||
|
ResultatCourse findByCourseId(Long courseId);
|
||||||
|
}
|
||||||
283
src/main/java/com/pmumali/quatro/service/ServiceQuatro.java
Normal file
283
src/main/java/com/pmumali/quatro/service/ServiceQuatro.java
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package com.pmumali.quatro.service;
|
||||||
|
|
||||||
|
import com.pmumali.quatro.model.*;
|
||||||
|
import com.pmumali.quatro.repository.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServiceQuatro {
|
||||||
|
|
||||||
|
private final PariRepository pariRepository;
|
||||||
|
private final CourseRepository courseRepository;
|
||||||
|
private final ChevalRepository chevalRepository;
|
||||||
|
private final PaiementRepository paiementRepository;
|
||||||
|
private final ParieurRepository parieurRepository;
|
||||||
|
private final ResultatCourseRepository resultatCourseRepository;
|
||||||
|
|
||||||
|
private static final double MISE_BASE = 500.0;
|
||||||
|
private static final double MISE_UNITAIRE_MAX = 20 * MISE_BASE;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Pari placerPari(RequetePari requete) {
|
||||||
|
// Validation de la mise
|
||||||
|
if (requete.getMise() < MISE_BASE) {
|
||||||
|
throw new IllegalArgumentException("La mise doit être au moins " + MISE_BASE + " FCFA");
|
||||||
|
}
|
||||||
|
|
||||||
|
Course course = courseRepository.findById(requete.getCourseId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
|
||||||
|
|
||||||
|
List<Cheval> chevaux = chevalRepository.findAllById(requete.getChevalIds());
|
||||||
|
|
||||||
|
if (chevaux.size() != 4) {
|
||||||
|
throw new IllegalArgumentException("Exactement 4 chevaux doivent être sélectionnés");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des chevaux non-partants
|
||||||
|
long nombreNonPartants = chevaux.stream().filter(Cheval::isNonPartant).count();
|
||||||
|
if (nombreNonPartants > 0) {
|
||||||
|
throw new IllegalArgumentException("Impossible de parier sur des chevaux non-partants");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limitation de la mise selon l'article 2
|
||||||
|
double miseEffective = Math.min(requete.getMise(), MISE_UNITAIRE_MAX);
|
||||||
|
|
||||||
|
Parieur parieur = parieurRepository.findById(requete.getParieurId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Parieur non trouvé"));
|
||||||
|
|
||||||
|
Pari pari = Pari.builder()
|
||||||
|
.course(course)
|
||||||
|
.chevauxSelectionnes(chevaux)
|
||||||
|
.mise(miseEffective)
|
||||||
|
.heurePari(LocalDateTime.now())
|
||||||
|
.typePari(requete.getTypePari())
|
||||||
|
.parieur(parieur)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return pariRepository.save(pari);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<ReponsePaiement> calculerPaiements(RequeteResultatCourse requeteResultat) {
|
||||||
|
ResultatCourse resultat = creerResultatCourse(requeteResultat);
|
||||||
|
List<Pari> tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId());
|
||||||
|
List<ReponsePaiement> paiements = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Pari pari : tousLesParis) {
|
||||||
|
ReponsePaiement paiement = calculerPaiementPourPari(pari, resultat);
|
||||||
|
if (paiement != null) {
|
||||||
|
paiements.add(paiement);
|
||||||
|
creerEnregistrementPaiement(pari, paiement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paiements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement calculerPaiementPourPari(Pari pari, ResultatCourse resultat) {
|
||||||
|
List<Cheval> chevauxSelectionnes = pari.getChevauxSelectionnes();
|
||||||
|
long nombreNonPartants = chevauxSelectionnes.stream().filter(Cheval::isNonPartant).count();
|
||||||
|
|
||||||
|
// Article 4: Cas des non-partants
|
||||||
|
if (nombreNonPartants >= 1) {
|
||||||
|
return gererCasNonPartants(pari, resultat, nombreNonPartants);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si tous les chevaux sélectionnés sont dans les 4 premières positions
|
||||||
|
boolean tousEnTop4 = chevauxSelectionnes.stream().allMatch(cheval ->
|
||||||
|
estChevalEnPosition(cheval, resultat.getPremiers()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getSeconds()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getTroisiemes()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getQuatriemes()));
|
||||||
|
|
||||||
|
if (tousEnTop4) {
|
||||||
|
double part = calculerPartQuatro(resultat);
|
||||||
|
return new ReponsePaiement(pari.getId(), part, TypePaiement.QUATRO, "Paiement QUATRO");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReponsePaiement gererCasNonPartants(Pari pari, ResultatCourse resultat, long nombreNonPartants) {
|
||||||
|
List<Cheval> chevauxParticipants = pari.getChevauxSelectionnes().stream()
|
||||||
|
.filter(cheval -> !cheval.isNonPartant())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
switch ((int) nombreNonPartants) {
|
||||||
|
case 1:
|
||||||
|
if (chevauxParticipants.size() == 3 &&
|
||||||
|
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 3)) {
|
||||||
|
double partQuatro = calculerPartQuatro(resultat);
|
||||||
|
double partSpecialTrio = partQuatro / 4; // 1/4 du QUATRO
|
||||||
|
return new ReponsePaiement(pari.getId(), partSpecialTrio,
|
||||||
|
TypePaiement.SPECIAL_TRIO, "Paiement Spécial Trio");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if (chevauxParticipants.size() == 2 &&
|
||||||
|
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 2)) {
|
||||||
|
double partQuatro = calculerPartQuatro(resultat);
|
||||||
|
double partSpecialJumele = partQuatro / 8; // 1/8 du QUATRO
|
||||||
|
return new ReponsePaiement(pari.getId(), partSpecialJumele,
|
||||||
|
TypePaiement.SPECIAL_JUMELE, "Paiement Spécial Jumelé");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
if (chevauxParticipants.size() == 1 &&
|
||||||
|
sontChevauxEnPositionsTop(chevauxParticipants, resultat, 1)) {
|
||||||
|
double partQuatro = calculerPartQuatro(resultat);
|
||||||
|
double partSpecialGagnant = partQuatro / 16; // 1/16 du QUATRO
|
||||||
|
return new ReponsePaiement(pari.getId(), partSpecialGagnant,
|
||||||
|
TypePaiement.SPECIAL_GAGNANT, "Paiement Spécial Gagnant");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return new ReponsePaiement(pari.getId(), pari.getMise(),
|
||||||
|
TypePaiement.REMBOURSEMENT, "Remboursement complet - tous chevaux non-partants");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sontChevauxEnPositionsTop(List<Cheval> chevaux, ResultatCourse resultat, int positionTop) {
|
||||||
|
return chevaux.stream().allMatch(cheval ->
|
||||||
|
estChevalEnPosition(cheval, resultat.getPremiers()) ||
|
||||||
|
(positionTop >= 2 && estChevalEnPosition(cheval, resultat.getSeconds())) ||
|
||||||
|
(positionTop >= 3 && estChevalEnPosition(cheval, resultat.getTroisiemes())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estChevalEnPosition(Cheval cheval, List<Cheval> chevauxPosition) {
|
||||||
|
return chevauxPosition != null && chevauxPosition.contains(cheval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculerPartQuatro(ResultatCourse resultat) {
|
||||||
|
double masseAPartager = resultat.getRecetteNette() - resultat.getMontantRembourse() - resultat.getPrelevementsLegaux();
|
||||||
|
// Calcul simplifié - l'implémentation réelle considérerait le nombre de paris gagnants
|
||||||
|
return masseAPartager; // Devrait être distribué entre les paris gagnants
|
||||||
|
}
|
||||||
|
|
||||||
|
private void creerEnregistrementPaiement(Pari pari, ReponsePaiement paiement) {
|
||||||
|
Paiement enregistrementPaiement = Paiement.builder()
|
||||||
|
.pari(pari)
|
||||||
|
.montant(paiement.getMontant())
|
||||||
|
.heurePaiement(LocalDateTime.now())
|
||||||
|
.typePaiement(paiement.getTypePaiement())
|
||||||
|
.build();
|
||||||
|
paiementRepository.save(enregistrementPaiement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultatCourse creerResultatCourse(RequeteResultatCourse requete) {
|
||||||
|
Course course = courseRepository.findById(requete.getCourseId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Course non trouvée"));
|
||||||
|
|
||||||
|
ResultatCourse resultat = ResultatCourse.builder()
|
||||||
|
.course(course)
|
||||||
|
.premiers(chevalRepository.findAllById(requete.getChevauxPremiers()))
|
||||||
|
.seconds(chevalRepository.findAllById(requete.getChevauxSeconds()))
|
||||||
|
.troisiemes(chevalRepository.findAllById(requete.getChevauxTroisiemes()))
|
||||||
|
.quatriemes(chevalRepository.findAllById(requete.getChevauxQuatriemes()))
|
||||||
|
.recetteNette(requete.getRecetteNette())
|
||||||
|
.prelevementsLegaux(requete.getPrelevementsLegaux())
|
||||||
|
.montantRembourse(0.0) // Serait calculé basé sur les remboursements
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return resultatCourseRepository.save(resultat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour gérer les dead-heat (Article 3)
|
||||||
|
@Transactional
|
||||||
|
public List<ReponsePaiement> gererDeadHeat(RequeteResultatCourse requeteResultat) {
|
||||||
|
ResultatCourse resultat = creerResultatCourse(requeteResultat);
|
||||||
|
List<Pari> tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId());
|
||||||
|
List<ReponsePaiement> paiements = new ArrayList<>();
|
||||||
|
|
||||||
|
// Implémentation des règles complexes de dead-heat
|
||||||
|
Map<String, List<Pari>> combinaisonsPayables = identifierCombinaisonsPayablesDeadHeat(resultat, tousLesParis);
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<Pari>> entry : combinaisonsPayables.entrySet()) {
|
||||||
|
double part = calculerPartDeadHeat(resultat, entry.getValue());
|
||||||
|
for (Pari pari : entry.getValue()) {
|
||||||
|
ReponsePaiement paiement = new ReponsePaiement(pari.getId(), part, TypePaiement.QUATRO, "Paiement Dead-Heat");
|
||||||
|
paiements.add(paiement);
|
||||||
|
creerEnregistrementPaiement(pari, paiement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paiements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<Pari>> identifierCombinaisonsPayablesDeadHeat(ResultatCourse resultat, List<Pari> paris) {
|
||||||
|
Map<String, List<Pari>> combinaisons = new HashMap<>();
|
||||||
|
|
||||||
|
for (Pari pari : paris) {
|
||||||
|
String combinaison = genererCleCombinaison(pari.getChevauxSelectionnes());
|
||||||
|
if (estCombinaisonPayableDeadHeat(pari.getChevauxSelectionnes(), resultat)) {
|
||||||
|
combinaisons.computeIfAbsent(combinaison, k -> new ArrayList<>()).add(pari);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinaisons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean estCombinaisonPayableDeadHeat(List<Cheval> chevaux, ResultatCourse resultat) {
|
||||||
|
// Implémentation des règles spécifiques de dead-heat selon l'article 3
|
||||||
|
// Cette méthode devrait implémenter toutes les sous-règles a) à h)
|
||||||
|
return chevaux.stream().allMatch(cheval ->
|
||||||
|
estChevalEnPosition(cheval, resultat.getPremiers()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getSeconds()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getTroisiemes()) ||
|
||||||
|
estChevalEnPosition(cheval, resultat.getQuatriemes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String genererCleCombinaison(List<Cheval> chevaux) {
|
||||||
|
return chevaux.stream()
|
||||||
|
.map(cheval -> String.valueOf(cheval.getId()))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.joining("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculerPartDeadHeat(ResultatCourse resultat, List<Pari> parisCombinaison) {
|
||||||
|
double masseAPartager = resultat.getMassePartager();
|
||||||
|
double totalMisesCombinaison = parisCombinaison.stream().mapToDouble(Pari::getMise).sum();
|
||||||
|
return (masseAPartager / parisCombinaison.size()) * (totalMisesCombinaison / masseAPartager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour les formules de pari (Article 6)
|
||||||
|
public int calculerNombreCombinaisons(TypePari typePari, int nombreChevaux) {
|
||||||
|
switch (typePari) {
|
||||||
|
case UNITAIRE:
|
||||||
|
return 1;
|
||||||
|
case COMBINE:
|
||||||
|
return (nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3)) / 24;
|
||||||
|
case CHAMP_TOTAL_3:
|
||||||
|
return 24 * (nombreChevaux - 3);
|
||||||
|
case CHAMP_PARTIEL_3:
|
||||||
|
return 24 * nombreChevaux;
|
||||||
|
case CHAMP_TOTAL_2:
|
||||||
|
return 12 * (nombreChevaux - 2) * (nombreChevaux - 3);
|
||||||
|
case CHAMP_PARTIEL_2:
|
||||||
|
return 12 * nombreChevaux * (nombreChevaux - 1);
|
||||||
|
case CHAMP_TOTAL_1:
|
||||||
|
return 4 * (nombreChevaux - 1) * (nombreChevaux - 2) * (nombreChevaux - 3);
|
||||||
|
case CHAMP_PARTIEL_1:
|
||||||
|
return 4 * nombreChevaux * (nombreChevaux - 1) * (nombreChevaux - 2);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double calculerValeurMise(TypePari typePari, int nombreChevaux) {
|
||||||
|
int nombreCombinaisons = calculerNombreCombinaisons(typePari, nombreChevaux);
|
||||||
|
return nombreCombinaisons * MISE_BASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.pmumali.simple.controller;
|
||||||
|
|
||||||
|
import com.pmumali.simple.model.Cheval;
|
||||||
|
import com.pmumali.simple.model.Course;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class CourseController {
|
||||||
|
|
||||||
|
private CourseDetailsDto convertToDetailsDto(Course course) {
|
||||||
|
CourseDetailsDto dto = new CourseDetailsDto();
|
||||||
|
dto.setId(course.getId());
|
||||||
|
dto.setNom(course.getNom());
|
||||||
|
dto.setDateCourse(course.getDateCourse());
|
||||||
|
dto.setChevaux(course.getChevaux().stream()
|
||||||
|
.map(this::convertChevalDto)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChevalDto convertChevalDto(Cheval cheval) {
|
||||||
|
ChevalDto dto = new ChevalDto();
|
||||||
|
dto.setId(cheval.getId());
|
||||||
|
dto.setNom(cheval.getNom());
|
||||||
|
dto.setNonPartant(cheval.isEstNonPartant());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.pmumali.simple.controller;
|
||||||
|
|
||||||
|
import com.pmu.jumele.dto.PariRequest;
|
||||||
|
import com.pmu.mali.model.ResultatCourse;
|
||||||
|
import com.pmumali.simple.dto.PariResponse;
|
||||||
|
import com.pmumali.simple.dto.ResultatCourseDto;
|
||||||
|
import com.pmumali.simple.model.Pari;
|
||||||
|
import com.pmumali.simple.service.PariService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/paris")
|
||||||
|
public class PariController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PariService pariService;
|
||||||
|
|
||||||
|
@PostMapping("/jumelé")
|
||||||
|
public ResponseEntity<Pari> placerPari(
|
||||||
|
@RequestBody PariRequest pariRequest) {
|
||||||
|
|
||||||
|
Pari pari = pariService.placerPariJumele(
|
||||||
|
pariRequest.getClientId(),
|
||||||
|
pariRequest.getCourseId(),
|
||||||
|
pariRequest.getChevauxIds(),
|
||||||
|
pariRequest.getMontantMise()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(pari);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/resultats/{courseId}")
|
||||||
|
public ResponseEntity<ResultatCourse> calculerResultats(
|
||||||
|
@PathVariable Long courseId) {
|
||||||
|
return ResponseEntity.ok(pariService.calculerResultats(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/jumele-place")
|
||||||
|
public ResponseEntity<PariResponse> placerPariJumelePlace(
|
||||||
|
@Valid @RequestBody PariRequest pariRequest) {
|
||||||
|
|
||||||
|
Pari pari = pariService.placerPariJumelePlace(pariRequest);
|
||||||
|
return ResponseEntity.ok(convertToResponse(pari));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/resultats/jumele-place/{courseId}")
|
||||||
|
public ResponseEntity<ResultatCourseDto> getResultatsJumelePlace(
|
||||||
|
@PathVariable Long courseId) {
|
||||||
|
Pari
|
||||||
|
ResultatCourseDto resultat = pariService.calculerResultatsJumelePlace(courseId);
|
||||||
|
return ResponseEntity.ok(resultat);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private PariResponse convertToResponse(Pari pari) {
|
||||||
|
PariResponse response = new PariResponse();
|
||||||
|
response.setId(pari.getId());
|
||||||
|
response.setMontantMise(pari.getMontantMise());
|
||||||
|
response.setDatePari(pari.getDatePari());
|
||||||
|
response.setChevaux(pari.getChevauxJumeles().stream()
|
||||||
|
.map(Cheval::getNom)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
response.setCourseNom(pari.getCourse().getNom());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/com/pmumali/simple/dto/ChevalDto.java
Normal file
23
src/main/java/com/pmumali/simple/dto/ChevalDto.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.pmumali.simple.dto;
|
||||||
|
|
||||||
|
import com.pmumali.simple.model.Cheval;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChevalDto {
|
||||||
|
private Long id;
|
||||||
|
private String nom;
|
||||||
|
private int numero;
|
||||||
|
private boolean estNonPartant;
|
||||||
|
|
||||||
|
// Constructeurs, Getters, Setters
|
||||||
|
|
||||||
|
public static ChevalDto fromEntity(Cheval cheval) {
|
||||||
|
ChevalDto dto = new ChevalDto();
|
||||||
|
dto.setId(cheval.getId());
|
||||||
|
dto.setNom(cheval.getNom());
|
||||||
|
dto.setNumero(cheval.getNumero());
|
||||||
|
dto.setEstNonPartant(cheval.isEstNonPartant());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user