commit 69b0fad8e87791c572c64ad0b9c2f0d17cfa368f Author: sidibe Date: Mon Aug 25 18:26:02 2025 +0000 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d8fc971 --- /dev/null +++ b/build.gradle @@ -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() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..bf53d0a --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'API-PLR' diff --git a/src/main/java/com/pmu/jumele/controller/JumeleController.java b/src/main/java/com/pmu/jumele/controller/JumeleController.java new file mode 100644 index 0000000..819f861 --- /dev/null +++ b/src/main/java/com/pmu/jumele/controller/JumeleController.java @@ -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 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 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 + } +} diff --git a/src/main/java/com/pmu/jumele/dto/PaiementResponse.java b/src/main/java/com/pmu/jumele/dto/PaiementResponse.java new file mode 100644 index 0000000..fa34e6d --- /dev/null +++ b/src/main/java/com/pmu/jumele/dto/PaiementResponse.java @@ -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; +} diff --git a/src/main/java/com/pmu/jumele/dto/PariRequest.java b/src/main/java/com/pmu/jumele/dto/PariRequest.java new file mode 100644 index 0000000..20a6465 --- /dev/null +++ b/src/main/java/com/pmu/jumele/dto/PariRequest.java @@ -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 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 champSelection; // used for champ partiel (if formuleType == champ_partiel) +} diff --git a/src/main/java/com/pmu/jumele/dto/PositionsRequest.java b/src/main/java/com/pmu/jumele/dto/PositionsRequest.java new file mode 100644 index 0000000..551fd71 --- /dev/null +++ b/src/main/java/com/pmu/jumele/dto/PositionsRequest.java @@ -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> positions; + private List nonPartants; // optional +} diff --git a/src/main/java/com/pmu/jumele/entity/PariEntity.java b/src/main/java/com/pmu/jumele/entity/PariEntity.java new file mode 100644 index 0000000..7037655 --- /dev/null +++ b/src/main/java/com/pmu/jumele/entity/PariEntity.java @@ -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; +} diff --git a/src/main/java/com/pmu/jumele/repository/PariRepository.java b/src/main/java/com/pmu/jumele/repository/PariRepository.java new file mode 100644 index 0000000..2468abb --- /dev/null +++ b/src/main/java/com/pmu/jumele/repository/PariRepository.java @@ -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 { + List findByCourseId(String courseId); + List findByCourseIdAndParieurAndCombinaison(String courseId, String parieur, String combinaison); +} diff --git a/src/main/java/com/pmu/jumele/service/JumeleGagnantService.java b/src/main/java/com/pmu/jumele/service/JumeleGagnantService.java new file mode 100644 index 0000000..5cf63b6 --- /dev/null +++ b/src/main/java/com/pmu/jumele/service/JumeleGagnantService.java @@ -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 enregistrerPari(PariRequest req) { + Map result = new HashMap<>(); + // validation minimale + if (req.getMise() < MISE_MIN.intValue()) throw new IllegalArgumentException("Mise minimum = 500"); + + // calculer unités + Set> 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 enregistrés = new ArrayList<>(); + + for (Set comb : combsToPlace) { + String combStr = CombinaisonUtil.combToString(comb); + // somme déjà engagée par ce parieur sur cette combinaison dans la même course + List 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 calculer(PositionsRequest positionsReq) { + Map out = new HashMap<>(); + String courseId = positionsReq.getCourseId(); + List parisCourse = pariRepository.findByCourseId(courseId); + List 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> positions = positionsReq.getPositions(); + if (positions == null || positions.size() < 2) { + // moins de deux classés => tout remboursé (Article 8) + List 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> combPayables = new HashSet<>(); + // a) dead-heat premiers (positions[0] size >=2) -> toutes combinaisons 2-à-2 parmi premiers + List 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 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> 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, BigDecimal> misesParComb = new HashMap<>(); + for (Set 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> combActives = misesParComb.entrySet().stream() + .filter(e -> e.getValue().compareTo(BigDecimal.ZERO) > 0) + .map(Map.Entry::getKey).collect(Collectors.toList()); + + BigDecimal cagnotte = BigDecimal.ZERO; + Map gains = new HashMap<>(); + + if (combActives.isEmpty()) { + cagnotte = cagnotte.add(map); + } else if (combActives.size() == 1) { + // cas normal unique combinaison + Set 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 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 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 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 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 stringToComb(String s) { + return Arrays.stream(s.split(",")).map(Integer::valueOf).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/pmu/jumele/service/JumeleGagnantServiceTest.java b/src/main/java/com/pmu/jumele/service/JumeleGagnantServiceTest.java new file mode 100644 index 0000000..33ec928 --- /dev/null +++ b/src/main/java/com/pmu/jumele/service/JumeleGagnantServiceTest.java @@ -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 res = svc.calculer(pos); + // assertThat(res).containsKey("paiements"); + } +} diff --git a/src/main/java/com/pmu/jumele/util/CombinaisonUtil.java b/src/main/java/com/pmu/jumele/util/CombinaisonUtil.java new file mode 100644 index 0000000..0bc65c6 --- /dev/null +++ b/src/main/java/com/pmu/jumele/util/CombinaisonUtil.java @@ -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> allPairs(List chevaux) { + Set> 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 -> "a,b" sorted string + public static String combToString(Set comb) { + return comb.stream().sorted().map(Object::toString).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/pmu/jumele/util/FormulesTable.java b/src/main/java/com/pmu/jumele/util/FormulesTable.java new file mode 100644 index 0000000..42c3a14 --- /dev/null +++ b/src/main/java/com/pmu/jumele/util/FormulesTable.java @@ -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 COMBINED_COMPLETE = new HashMap<>(); + public static final Map COMBINED_SIMPLE = new HashMap<>(); + public static final Map CHAMP_TOTAL = new HashMap<>(); + public static final Map 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)); + // ... + } +} diff --git a/src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java b/src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java new file mode 100644 index 0000000..44ce885 --- /dev/null +++ b/src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java @@ -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); + } + +} diff --git a/src/main/java/com/pmu/mali/model/Pari.java b/src/main/java/com/pmu/mali/model/Pari.java new file mode 100644 index 0000000..039a980 --- /dev/null +++ b/src/main/java/com/pmu/mali/model/Pari.java @@ -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) +} diff --git a/src/main/java/com/pmu/mali/model/ResultatCourse.java b/src/main/java/com/pmu/mali/model/ResultatCourse.java new file mode 100644 index 0000000..a0f3c2c --- /dev/null +++ b/src/main/java/com/pmu/mali/model/ResultatCourse.java @@ -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 premiers; // un ou plusieurs (dead-heat) + private List deuxiemes; // peut être vide + private List troisiemes; // peut être vide + private int nombrePartants; // nombre de chevaux engagés (programme officiel) +} diff --git a/src/main/java/com/pmu/mali/model/TypePari.java b/src/main/java/com/pmu/mali/model/TypePari.java new file mode 100644 index 0000000..2dcc88b --- /dev/null +++ b/src/main/java/com/pmu/mali/model/TypePari.java @@ -0,0 +1,6 @@ +package com.pmu.mali.model; + +public enum TypePari { + SIMPLE_GAGNANT, + SIMPLE_PLACE +} diff --git a/src/main/java/com/pmu/mali/service/CalculPariService.java b/src/main/java/com/pmu/mali/service/CalculPariService.java new file mode 100644 index 0000000..0789f22 --- /dev/null +++ b/src/main/java/com/pmu/mali/service/CalculPariService.java @@ -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 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 paris, ResultatCourse resultat, BigDecimal prelevements) { + ResultatCalcul res = new ResultatCalcul(); + + if (prelevements == null) prelevements = BigDecimal.ZERO; + + // Séparer paris par type + List parisGagnant = filterByType(paris, TypePari.SIMPLE_GAGNANT); + List 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 filterByType(List all, TypePari t) { + return all.stream().filter(p -> p.getType() == t).collect(Collectors.toList()); + } + + private BigDecimal totalMise(List 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 list, ResultatCalcul res) { + return rembourserNonPartants(list, res.gainsParParieur); + } + + private BigDecimal rembourserNonPartants(List list, Map 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 parisGagnant, ResultatCourse resultat, BigDecimal masse, ResultatCalcul res) { + List 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 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 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 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 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 set = new LinkedHashSet<>(payables); + List 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 premiers = resultat.getPremiers(); + repartirPlacePosition(parisPlace, premiers, partParCombinaison, res); + + if (nbPlacesPayees >= 2) { + List deuxiemes = resultat.getDeuxiemes(); + repartirPlacePosition(parisPlace, deuxiemes, partParCombinaison, res); + } + if (nbPlacesPayees >= 3) { + List 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 parisPlace, List 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)); + } + } + } + } +} diff --git a/src/main/java/com/pmumali/jumeleordre/controller/CourseController.java b/src/main/java/com/pmumali/jumeleordre/controller/CourseController.java new file mode 100644 index 0000000..5ea9ab5 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/controller/CourseController.java @@ -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> getCoursesActives() { + List courses = courseService.getCoursesActives(); + return ResponseEntity.ok(courses); + } + + @PostMapping("/resultat") + public ResponseEntity soumettreResultatCourse(@RequestBody ResultatCourseDto resultatDto) { + try { + resultatCourseService.traiterResultatCourse(resultatDto); + return ResponseEntity.ok().build(); + } catch (ResultatCourseInvalideException e) { + return ResponseEntity.badRequest().build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/controller/ParisController.java b/src/main/java/com/pmumali/jumeleordre/controller/ParisController.java new file mode 100644 index 0000000..2180803 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/controller/ParisController.java @@ -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 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> getParisParParieur(@PathVariable String idParieur) { + List paris = parisService.getParisParParieur(idParieur); + return ResponseEntity.ok(paris); + } + + @GetMapping("/gains/{idParis}") + public ResponseEntity getGains(@PathVariable Long idParis) { + // Implémentation pour obtenir les détails des gains d'un pari + return ResponseEntity.ok(new GainsDto()); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java b/src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java new file mode 100644 index 0000000..51d4152 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java b/src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java new file mode 100644 index 0000000..5024d78 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/dto/ResultatCourseDto.java b/src/main/java/com/pmumali/jumeleordre/dto/ResultatCourseDto.java new file mode 100644 index 0000000..1451a3c --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/dto/ResultatCourseDto.java @@ -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 chevauxDeadHeat; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/ChevalInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/ChevalInvalideException.java new file mode 100644 index 0000000..2487ab9 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/ChevalInvalideException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/CourseDejaTermineeException.java b/src/main/java/com/pmumali/jumeleordre/exception/CourseDejaTermineeException.java new file mode 100644 index 0000000..8cfe183 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/CourseDejaTermineeException.java @@ -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)); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/CourseInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/CourseInvalideException.java new file mode 100644 index 0000000..85cb80c --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/CourseInvalideException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/GlobalExceptionHandler.java b/src/main/java/com/pmumali/jumeleordre/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f2bd759 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/GlobalExceptionHandler.java @@ -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 handleJumeleOrdreException( + JumeleOrdreException ex, WebRequest request) { + + Map 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 handleGlobalException( + Exception ex, WebRequest request) { + + Map 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/JumeleOrdreException.java b/src/main/java/com/pmumali/jumeleordre/exception/JumeleOrdreException.java new file mode 100644 index 0000000..1231af1 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/JumeleOrdreException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/LimiteMiseDepasseeException.java b/src/main/java/com/pmumali/jumeleordre/exception/LimiteMiseDepasseeException.java new file mode 100644 index 0000000..1ee9dfd --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/LimiteMiseDepasseeException.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/NombreChevauxInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/NombreChevauxInvalideException.java new file mode 100644 index 0000000..4073e77 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/NombreChevauxInvalideException.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/PaiementInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/PaiementInvalideException.java new file mode 100644 index 0000000..2430bc9 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/PaiementInvalideException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/ParisInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/ParisInvalideException.java new file mode 100644 index 0000000..dd29ab8 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/ParisInvalideException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/ResultatCourseInvalideException.java b/src/main/java/com/pmumali/jumeleordre/exception/ResultatCourseInvalideException.java new file mode 100644 index 0000000..6af9e40 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/ResultatCourseInvalideException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/exception/ValidationException.java b/src/main/java/com/pmumali/jumeleordre/exception/ValidationException.java new file mode 100644 index 0000000..33529e5 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/exception/ValidationException.java @@ -0,0 +1,17 @@ +// ValidationException.java +package com.pmumali.jumeleordre.exception; + +import java.util.List; + +public class ValidationException extends JumeleOrdreException { + private final List erreurs; + + public ValidationException(String message, List erreurs) { + super(message); + this.erreurs = erreurs; + } + + public List getErreurs() { + return erreurs; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/model/Cheval.java b/src/main/java/com/pmumali/jumeleordre/model/Cheval.java new file mode 100644 index 0000000..a3806e5 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/model/Cheval.java @@ -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; +} diff --git a/src/main/java/com/pmumali/jumeleordre/model/Course.java b/src/main/java/com/pmumali/jumeleordre/model/Course.java new file mode 100644 index 0000000..f3efebb --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/model/Course.java @@ -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 chevaux; + private boolean estTerminee; + private boolean aDeadHeat; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/model/Paris.java b/src/main/java/com/pmumali/jumeleordre/model/Paris.java new file mode 100644 index 0000000..27fe6de --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/model/Paris.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/model/ResultatCourse.java b/src/main/java/com/pmumali/jumeleordre/model/ResultatCourse.java new file mode 100644 index 0000000..0f680a7 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/model/ResultatCourse.java @@ -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 chevauxDeadHeat; + + private double totalMises; + private double masseAPartager; + private double prelevementsLegaux; + private double montantRembourse; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/repository/ChevalRepository.java b/src/main/java/com/pmumali/jumeleordre/repository/ChevalRepository.java new file mode 100644 index 0000000..9f5c63e --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/repository/ChevalRepository.java @@ -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 { +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/repository/CourseRepository.java b/src/main/java/com/pmumali/jumeleordre/repository/CourseRepository.java new file mode 100644 index 0000000..f2e851b --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/repository/CourseRepository.java @@ -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 { + List findByEstTermineeFalse(); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/repository/ParisRepository.java b/src/main/java/com/pmumali/jumeleordre/repository/ParisRepository.java new file mode 100644 index 0000000..96f9b09 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/repository/ParisRepository.java @@ -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 { + List findByCourseIdAndStatut(Long idCourse, Paris.StatutParis statut); + List findByIdParieur(String idParieur); + + List findByCourseId(Long courseID); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/jumeleordre/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..c289b03 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/repository/ResultatCourseRepository.java @@ -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 findByCourseId(Long idCourse); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/service/CourseService.java b/src/main/java/com/pmumali/jumeleordre/service/CourseService.java new file mode 100644 index 0000000..dd944bc --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/service/CourseService.java @@ -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 getCoursesActives() { + return courseRepository.findByEstTermineeFalse(); + } + + /** + * Crée une nouvelle course + */ + public Course creerCourse(String nomCourse, LocalDateTime heureDebut, List 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 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 getAllCourses() { + return courseRepository.findAll(); + } + + /** + * Vérifie si une course existe + */ + public boolean courseExiste(Long id) { + return courseRepository.existsById(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/service/ParisService.java b/src/main/java/com/pmumali/jumeleordre/service/ParisService.java new file mode 100644 index 0000000..e337ff6 --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/service/ParisService.java @@ -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 getParisParParieur(String idParieur) { + return parisRepository.findByIdParieur(idParieur); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/jumeleordre/service/ResultatCourseService.java b/src/main/java/com/pmumali/jumeleordre/service/ResultatCourseService.java new file mode 100644 index 0000000..b42669a --- /dev/null +++ b/src/main/java/com/pmumali/jumeleordre/service/ResultatCourseService.java @@ -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 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) { + return paris.stream() + .filter(p -> p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant()) + .mapToDouble(Paris::getMise) + .sum(); + } + + private void traiterParis(List 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 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) + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quarteplus/controller/ControleurQuartePlus.java b/src/main/java/com/pmumali/quarteplus/controller/ControleurQuartePlus.java new file mode 100644 index 0000000..1c08898 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/controller/ControleurQuartePlus.java @@ -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 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> calculerPaiements(@RequestBody RequeteResultat requete) { + try { + List paiements = service.calculerPaiements(requete); + return ResponseEntity.ok(paiements); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @PostMapping("/dead-heat/{type}") + public ResponseEntity> gererDeadHeat( + @RequestBody RequeteResultat requete, + @PathVariable TypeDeadHeat type) { + try { + List paiements = service.gererDeadHeat(requete, type); + return ResponseEntity.ok(paiements); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @GetMapping("/calcul-combinaison") + public ResponseEntity 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 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 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> getParisCourse(@PathVariable Long courseId) { + return ResponseEntity.ok(pariRepository.findByCourseId(courseId)); + } + + @GetMapping("/paiements/pari/{pariId}") + public ResponseEntity> getPaiementsPari(@PathVariable Long pariId) { + return ResponseEntity.ok(paiementRepository.findByPariId(pariId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quarteplus/exception/GestionnaireExceptions.java b/src/main/java/com/pmumali/quarteplus/exception/GestionnaireExceptions.java new file mode 100644 index 0000000..11b95ba --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/exception/GestionnaireExceptions.java @@ -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 handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception ex) { + return ResponseEntity.internalServerError().body("Erreur interne: " + ex.getMessage()); + } +} diff --git a/src/main/java/com/pmumali/quarteplus/model/Cagnotte.java b/src/main/java/com/pmumali/quarteplus/model/Cagnotte.java new file mode 100644 index 0000000..b2ee54d --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/Cagnotte.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/CalculCombinaison.java b/src/main/java/com/pmumali/quarteplus/model/CalculCombinaison.java new file mode 100644 index 0000000..d7783cd --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/CalculCombinaison.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/Cheval.java b/src/main/java/com/pmumali/quarteplus/model/Cheval.java new file mode 100644 index 0000000..8c848e1 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/Cheval.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quarteplus/model/Course.java b/src/main/java/com/pmumali/quarteplus/model/Course.java new file mode 100644 index 0000000..219c979 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/Course.java @@ -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 chevaux; + + @Enumerated(EnumType.STRING) + private StatutCourse statut; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/Paiement.java b/src/main/java/com/pmumali/quarteplus/model/Paiement.java new file mode 100644 index 0000000..f52b8ec --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/Paiement.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/PariQuartePlus.java b/src/main/java/com/pmumali/quarteplus/model/PariQuartePlus.java new file mode 100644 index 0000000..27dbe99 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/PariQuartePlus.java @@ -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 chevauxSelectionnes; + + private Double mise; + private LocalDateTime heurePari; + + @Enumerated(EnumType.STRING) + private TypePari typePari; + + @ManyToOne + private Parieur parieur; + + private Boolean validationOrdreExact; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/Parieur.java b/src/main/java/com/pmumali/quarteplus/model/Parieur.java new file mode 100644 index 0000000..dadc893 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/Parieur.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/ReponsePaiement.java b/src/main/java/com/pmumali/quarteplus/model/ReponsePaiement.java new file mode 100644 index 0000000..41e43ec --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/ReponsePaiement.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/RequetePari.java b/src/main/java/com/pmumali/quarteplus/model/RequetePari.java new file mode 100644 index 0000000..345e2d8 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/RequetePari.java @@ -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 chevalIds; + private Double mise; + private TypePari typePari; + private Long parieurId; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/RequeteResultat.java b/src/main/java/com/pmumali/quarteplus/model/RequeteResultat.java new file mode 100644 index 0000000..dd6d819 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/RequeteResultat.java @@ -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 ordreArriveeIds; + private List premiersIds; + private List secondsIds; + private List troisiemesIds; + private List quatriemesIds; + private Double recetteNette; + private Double prelevementsLegaux; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quarteplus/model/ResultatCourse.java b/src/main/java/com/pmumali/quarteplus/model/ResultatCourse.java new file mode 100644 index 0000000..fd3e7d7 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/ResultatCourse.java @@ -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 ordreArrivee; + + @ManyToMany + private List premiers; + + @ManyToMany + private List seconds; + + @ManyToMany + private List troisiemes; + + @ManyToMany + private List quatriemes; + + private Double recetteNette; + private Double montantRembourse; + private Double prelevementsLegaux; + private Double masseAPartager; +} diff --git a/src/main/java/com/pmumali/quarteplus/model/StatutCourse.java b/src/main/java/com/pmumali/quarteplus/model/StatutCourse.java new file mode 100644 index 0000000..875b53f --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/StatutCourse.java @@ -0,0 +1,5 @@ +package com.pmumali.quarteplus.model; + +public enum StatutCourse { + PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE +} diff --git a/src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java b/src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java new file mode 100644 index 0000000..e661c27 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java @@ -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 +} diff --git a/src/main/java/com/pmumali/quarteplus/model/TypePaiement.java b/src/main/java/com/pmumali/quarteplus/model/TypePaiement.java new file mode 100644 index 0000000..55d3551 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/TypePaiement.java @@ -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 +} diff --git a/src/main/java/com/pmumali/quarteplus/model/TypePari.java b/src/main/java/com/pmumali/quarteplus/model/TypePari.java new file mode 100644 index 0000000..e878749 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/model/TypePari.java @@ -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 +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/CagnotteRepository.java b/src/main/java/com/pmumali/quarteplus/repository/CagnotteRepository.java new file mode 100644 index 0000000..7832a1c --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/CagnotteRepository.java @@ -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 { + List findByUtilisee(Boolean utilisee); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/ChevalRepository.java b/src/main/java/com/pmumali/quarteplus/repository/ChevalRepository.java new file mode 100644 index 0000000..52ca01a --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/ChevalRepository.java @@ -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 { + List findByNonPartant(Boolean nonPartant); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/CourseRepository.java b/src/main/java/com/pmumali/quarteplus/repository/CourseRepository.java new file mode 100644 index 0000000..3119900 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/CourseRepository.java @@ -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 { + List findByStatut(StatutCourse statut); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/PaiementRepository.java b/src/main/java/com/pmumali/quarteplus/repository/PaiementRepository.java new file mode 100644 index 0000000..0745c03 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/PaiementRepository.java @@ -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 { + List findByPariId(Long pariId); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/PariQuartePlusRepository.java b/src/main/java/com/pmumali/quarteplus/repository/PariQuartePlusRepository.java new file mode 100644 index 0000000..5345d49 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/PariQuartePlusRepository.java @@ -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 { + List findByCourseId(Long courseId); + List findByParieurId(Long parieurId); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/ParieurRepository.java b/src/main/java/com/pmumali/quarteplus/repository/ParieurRepository.java new file mode 100644 index 0000000..1161b19 --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/ParieurRepository.java @@ -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 { + List findByNomContaining(String nom); +} diff --git a/src/main/java/com/pmumali/quarteplus/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/quarteplus/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..117badf --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/repository/ResultatCourseRepository.java @@ -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 findByCourseId(Long courseId); +} diff --git a/src/main/java/com/pmumali/quarteplus/service/ServiceQuartePlus.java b/src/main/java/com/pmumali/quarteplus/service/ServiceQuartePlus.java new file mode 100644 index 0000000..7abc9cc --- /dev/null +++ b/src/main/java/com/pmumali/quarteplus/service/ServiceQuartePlus.java @@ -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 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 calculerPaiements(RequeteResultat requete) { + ResultatCourse resultat = creerResultat(requete); + List paris = pariRepository.findByCourseId(requete.getCourseId()); + List 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 compteurs = new HashMap<>(); + Map 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 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 chevauxPari, List 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 liste) { + return liste != null && liste.contains(cheval); + } + + private boolean estEligibleBonus3(PariQuartePlus pari, ResultatCourse resultat) { + List 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 troisPremiersArrivee = resultat.getOrdreArrivee().subList(0, 3); + List 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 paiements, + Map compteurs, + Map 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 paiements, + Map 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 paiements, + Map compteurs, + Map 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 gererDeadHeat(RequeteResultat requete, TypeDeadHeat typeDeadHeat) { + ResultatCourse resultat = creerResultat(requete); + List paris = pariRepository.findByCourseId(requete.getCourseId()); + List 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 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 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 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 getCagnottesDisponibles() { + return cagnotteRepository.findByUtilisee(false); + } + + // Méthodes utilitaires supplémentaires + public Map getStatistiquesParis(Long courseId) { + List 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 getHistoriquePaiementsParieur(Long parieurId) { + List paris = pariRepository.findByParieurId(parieurId); + return paris.stream() + .flatMap(pari -> paiementRepository.findByPariId(pari.getId()).stream()) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quatro/controller/ControllerQuatro.java b/src/main/java/com/pmumali/quatro/controller/ControllerQuatro.java new file mode 100644 index 0000000..3d41f8e --- /dev/null +++ b/src/main/java/com/pmumali/quatro/controller/ControllerQuatro.java @@ -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 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> calculerPaiements(@RequestBody RequeteResultatCourse requeteResultat) { + try { + List paiements = serviceQuatro.calculerPaiements(requeteResultat); + return ResponseEntity.ok(paiements); + } catch (Exception e) { + return ResponseEntity.badRequest().body(null); + } + } + + @PostMapping("/gerer-dead-heat") + public ResponseEntity> gererDeadHeat(@RequestBody RequeteResultatCourse requeteResultat) { + try { + List paiements = serviceQuatro.gererDeadHeat(requeteResultat); + return ResponseEntity.ok(paiements); + } catch (Exception e) { + return ResponseEntity.badRequest().body(null); + } + } + + @GetMapping("/paris/course/{courseId}") + public ResponseEntity> getParisParCourse(@PathVariable Long courseId) { + return ResponseEntity.ok(pariRepository.findByCourseId(courseId)); + } + + @GetMapping("/paiements/pari/{pariId}") + public ResponseEntity> getPaiementsParPari(@PathVariable Long pariId) { + return ResponseEntity.ok(paiementRepository.findByPariId(pariId)); + } + + @GetMapping("/calcul-combinaisons") + public ResponseEntity calculerCombinaisons( + @RequestParam TypePari typePari, + @RequestParam int nombreChevaux) { + int nombreCombinaisons = serviceQuatro.calculerNombreCombinaisons(typePari, nombreChevaux); + return ResponseEntity.ok(nombreCombinaisons); + } + + @GetMapping("/calcul-valeur-mise") + public ResponseEntity calculerValeurMise( + @RequestParam TypePari typePari, + @RequestParam int nombreChevaux) { + double valeurMise = serviceQuatro.calculerValeurMise(typePari, nombreChevaux); + return ResponseEntity.ok(valeurMise); + } +} diff --git a/src/main/java/com/pmumali/quatro/exception/GestionnaireExceptionsGlobal.java b/src/main/java/com/pmumali/quatro/exception/GestionnaireExceptionsGlobal.java new file mode 100644 index 0000000..9c198d0 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/exception/GestionnaireExceptionsGlobal.java @@ -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 handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + return ResponseEntity.badRequest().body("Erreur: " + ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleExceptionGenerale(Exception ex) { + return ResponseEntity.internalServerError().body("Une erreur s'est produite: " + ex.getMessage()); + } +} diff --git a/src/main/java/com/pmumali/quatro/model/Cheval.java b/src/main/java/com/pmumali/quatro/model/Cheval.java new file mode 100644 index 0000000..dda773f --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Cheval.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quatro/model/Course.java b/src/main/java/com/pmumali/quatro/model/Course.java new file mode 100644 index 0000000..12979f4 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Course.java @@ -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 chevaux; + + @Enumerated(EnumType.STRING) + @Column(name = "statut") + private StatutCourse statut; +} diff --git a/src/main/java/com/pmumali/quatro/model/Paiement.java b/src/main/java/com/pmumali/quatro/model/Paiement.java new file mode 100644 index 0000000..247a6ca --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Paiement.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quatro/model/Pari.java b/src/main/java/com/pmumali/quatro/model/Pari.java new file mode 100644 index 0000000..92a7b40 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Pari.java @@ -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 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; +} diff --git a/src/main/java/com/pmumali/quatro/model/Parieur.java b/src/main/java/com/pmumali/quatro/model/Parieur.java new file mode 100644 index 0000000..d22250f --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Parieur.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quatro/model/Position.java b/src/main/java/com/pmumali/quatro/model/Position.java new file mode 100644 index 0000000..fd8c024 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/Position.java @@ -0,0 +1,5 @@ +package com.pmumali.quatro.model; + +public enum Position { + PREMIER, SECOND, TROISIEME, QUATRIEME +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/quatro/model/ReponsePaiement.java b/src/main/java/com/pmumali/quatro/model/ReponsePaiement.java new file mode 100644 index 0000000..6e4f232 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/ReponsePaiement.java @@ -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; +} diff --git a/src/main/java/com/pmumali/quatro/model/RequetePari.java b/src/main/java/com/pmumali/quatro/model/RequetePari.java new file mode 100644 index 0000000..d3bd200 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/RequetePari.java @@ -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 chevalIds; + private Double mise; + private TypePari typePari; + private Long parieurId; +} diff --git a/src/main/java/com/pmumali/quatro/model/RequeteResultatCourse.java b/src/main/java/com/pmumali/quatro/model/RequeteResultatCourse.java new file mode 100644 index 0000000..7fe3a01 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/RequeteResultatCourse.java @@ -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 chevauxPremiers; + private List chevauxSeconds; + private List chevauxTroisiemes; + private List chevauxQuatriemes; + private Double recetteNette; + private Double prelevementsLegaux; +} diff --git a/src/main/java/com/pmumali/quatro/model/ResultatCourse.java b/src/main/java/com/pmumali/quatro/model/ResultatCourse.java new file mode 100644 index 0000000..cd4fa99 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/ResultatCourse.java @@ -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 premiers; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "resultat_seconds", + joinColumns = @JoinColumn(name = "resultat_id"), + inverseJoinColumns = @JoinColumn(name = "cheval_id")) + private List seconds; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "resultat_troisiemes", + joinColumns = @JoinColumn(name = "resultat_id"), + inverseJoinColumns = @JoinColumn(name = "cheval_id")) + private List troisiemes; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "resultat_quatriemes", + joinColumns = @JoinColumn(name = "resultat_id"), + inverseJoinColumns = @JoinColumn(name = "cheval_id")) + private List 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; +} diff --git a/src/main/java/com/pmumali/quatro/model/StatutCourse.java b/src/main/java/com/pmumali/quatro/model/StatutCourse.java new file mode 100644 index 0000000..e39b988 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/StatutCourse.java @@ -0,0 +1,5 @@ +package com.pmumali.quatro.model; + +public enum StatutCourse { + PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE +} diff --git a/src/main/java/com/pmumali/quatro/model/TypePaiement.java b/src/main/java/com/pmumali/quatro/model/TypePaiement.java new file mode 100644 index 0000000..0eedeb5 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/TypePaiement.java @@ -0,0 +1,5 @@ +package com.pmumali.quatro.model; + +public enum TypePaiement { + QUATRO, SPECIAL_TRIO, SPECIAL_JUMELE, SPECIAL_GAGNANT, REMBOURSEMENT +} diff --git a/src/main/java/com/pmumali/quatro/model/TypePari.java b/src/main/java/com/pmumali/quatro/model/TypePari.java new file mode 100644 index 0000000..70f02fb --- /dev/null +++ b/src/main/java/com/pmumali/quatro/model/TypePari.java @@ -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 +} diff --git a/src/main/java/com/pmumali/quatro/repository/ChevalRepository.java b/src/main/java/com/pmumali/quatro/repository/ChevalRepository.java new file mode 100644 index 0000000..4363316 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/ChevalRepository.java @@ -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 { + List findByNonPartant(boolean nonPartant); + List findByIdIn(List ids); +} diff --git a/src/main/java/com/pmumali/quatro/repository/CourseRepository.java b/src/main/java/com/pmumali/quatro/repository/CourseRepository.java new file mode 100644 index 0000000..36ec37f --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/CourseRepository.java @@ -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 { + List findByStatut(StatutCourse statut); +} diff --git a/src/main/java/com/pmumali/quatro/repository/PaiementRepository.java b/src/main/java/com/pmumali/quatro/repository/PaiementRepository.java new file mode 100644 index 0000000..71ee9e2 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/PaiementRepository.java @@ -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 { + List findByPariId(Long pariId); + List findByPariCourseId(Long courseId); +} diff --git a/src/main/java/com/pmumali/quatro/repository/PariRepository.java b/src/main/java/com/pmumali/quatro/repository/PariRepository.java new file mode 100644 index 0000000..1ef3588 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/PariRepository.java @@ -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 { + List findByCourseId(Long courseId); + List findByParieurId(Long parieurId); + List findByCourseIdAndTypePari(Long courseId, TypePari typePari); +} diff --git a/src/main/java/com/pmumali/quatro/repository/ParieurRepository.java b/src/main/java/com/pmumali/quatro/repository/ParieurRepository.java new file mode 100644 index 0000000..a6a7e5f --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/ParieurRepository.java @@ -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 { + List findByNomContaining(String nom); +} diff --git a/src/main/java/com/pmumali/quatro/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/quatro/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..bcce8af --- /dev/null +++ b/src/main/java/com/pmumali/quatro/repository/ResultatCourseRepository.java @@ -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 findByCourseId(Long courseId); +} diff --git a/src/main/java/com/pmumali/quatro/service/ServiceQuatro.java b/src/main/java/com/pmumali/quatro/service/ServiceQuatro.java new file mode 100644 index 0000000..8bff964 --- /dev/null +++ b/src/main/java/com/pmumali/quatro/service/ServiceQuatro.java @@ -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 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 calculerPaiements(RequeteResultatCourse requeteResultat) { + ResultatCourse resultat = creerResultatCourse(requeteResultat); + List tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId()); + List 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 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 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 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 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 gererDeadHeat(RequeteResultatCourse requeteResultat) { + ResultatCourse resultat = creerResultatCourse(requeteResultat); + List tousLesParis = pariRepository.findByCourseId(requeteResultat.getCourseId()); + List paiements = new ArrayList<>(); + + // Implémentation des règles complexes de dead-heat + Map> combinaisonsPayables = identifierCombinaisonsPayablesDeadHeat(resultat, tousLesParis); + + for (Map.Entry> 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> identifierCombinaisonsPayablesDeadHeat(ResultatCourse resultat, List paris) { + Map> 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 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 chevaux) { + return chevaux.stream() + .map(cheval -> String.valueOf(cheval.getId())) + .sorted() + .collect(Collectors.joining("-")); + } + + private double calculerPartDeadHeat(ResultatCourse resultat, List 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; + } +} diff --git a/src/main/java/com/pmumali/simple/controller/CourseController.java b/src/main/java/com/pmumali/simple/controller/CourseController.java new file mode 100644 index 0000000..bf2c61a --- /dev/null +++ b/src/main/java/com/pmumali/simple/controller/CourseController.java @@ -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; + } +} diff --git a/src/main/java/com/pmumali/simple/controller/PariController.java b/src/main/java/com/pmumali/simple/controller/PariController.java new file mode 100644 index 0000000..0b4755c --- /dev/null +++ b/src/main/java/com/pmumali/simple/controller/PariController.java @@ -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 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 calculerResultats( + @PathVariable Long courseId) { + return ResponseEntity.ok(pariService.calculerResultats(courseId)); + } + + @PostMapping("/jumele-place") + public ResponseEntity placerPariJumelePlace( + @Valid @RequestBody PariRequest pariRequest) { + + Pari pari = pariService.placerPariJumelePlace(pariRequest); + return ResponseEntity.ok(convertToResponse(pari)); + } + + @GetMapping("/resultats/jumele-place/{courseId}") + public ResponseEntity 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; + } +} diff --git a/src/main/java/com/pmumali/simple/dto/ChevalDto.java b/src/main/java/com/pmumali/simple/dto/ChevalDto.java new file mode 100644 index 0000000..7781863 --- /dev/null +++ b/src/main/java/com/pmumali/simple/dto/ChevalDto.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/dto/CombinaisonDto.java b/src/main/java/com/pmumali/simple/dto/CombinaisonDto.java new file mode 100644 index 0000000..dbe232b --- /dev/null +++ b/src/main/java/com/pmumali/simple/dto/CombinaisonDto.java @@ -0,0 +1,12 @@ +package com.pmumali.simple.dto; + +import lombok.Data; + +@Data +public class CombinaisonDto { + private String cheval1; + private String cheval2; + private int nombreMises; + + // Getters, setters +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/dto/PariRequest.java b/src/main/java/com/pmumali/simple/dto/PariRequest.java new file mode 100644 index 0000000..ef61be5 --- /dev/null +++ b/src/main/java/com/pmumali/simple/dto/PariRequest.java @@ -0,0 +1,15 @@ +package com.pmumali.simple.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class PariRequest { + private Long clientId; + private Long courseId; + private List chevauxIds; + private double montantMise; + + // Getters, setters +} diff --git a/src/main/java/com/pmumali/simple/dto/PariResponse.java b/src/main/java/com/pmumali/simple/dto/PariResponse.java new file mode 100644 index 0000000..a7c691f --- /dev/null +++ b/src/main/java/com/pmumali/simple/dto/PariResponse.java @@ -0,0 +1,211 @@ +package com.pmumali.simple.dto; + +import com.pmumali.simple.model.Cheval; +import com.pmumali.simple.model.Pari; +import com.pmumali.simple.model.enums.TypePari; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +/** + * DTO pour la réponse API des paris + * Contient toutes les informations à exposer au client + */ +public class PariResponse { + private Long id; + private String numeroPari; + private TypePari typePari; + private double montantMise; + + @JsonFormat(pattern = "dd/MM/yyyy HH:mm:ss") + private LocalDateTime datePari; + + private String statut; // EN_COURS, GAGNANT, PERDANT, REMBOURSE + private Double gainsPotentiels; + private boolean estPaye; + + // Informations course + private Long courseId; + private String courseNom; + @JsonFormat(pattern = "dd/MM/yyyy HH:mm") + private LocalDateTime dateCourse; + + // Chevaux sélectionnés + private List chevaux; + + // Informations client (simplifiées) + private String clientNomComplet; + private String clientNumero; + + // Constructeurs + public PariResponse() {} + + public PariResponse(Long id, String numeroPari, TypePari typePari, double montantMise, + LocalDateTime datePari, String statut, Double gainsPotentiels, + boolean estPaye, Long courseId, String courseNom, + LocalDateTime dateCourse) { + this.id = id; + this.numeroPari = numeroPari; + this.typePari = typePari; + this.montantMise = montantMise; + this.datePari = datePari; + this.statut = statut; + this.gainsPotentiels = gainsPotentiels; + this.estPaye = estPaye; + this.courseId = courseId; + this.courseNom = courseNom; + this.dateCourse = dateCourse; + } + + // Getters et Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getNumeroPari() { + return numeroPari; + } + + public void setNumeroPari(String numeroPari) { + this.numeroPari = numeroPari; + } + + public TypePari getTypePari() { + return typePari; + } + + public void setTypePari(TypePari typePari) { + this.typePari = typePari; + } + + public double getMontantMise() { + return montantMise; + } + + public void setMontantMise(double montantMise) { + this.montantMise = montantMise; + } + + public LocalDateTime getDatePari() { + return datePari; + } + + public void setDatePari(LocalDateTime datePari) { + this.datePari = datePari; + } + + public String getStatut() { + return statut; + } + + public void setStatut(String statut) { + this.statut = statut; + } + + public Double getGainsPotentiels() { + return gainsPotentiels; + } + + public void setGainsPotentiels(Double gainsPotentiels) { + this.gainsPotentiels = gainsPotentiels; + } + + public boolean isEstPaye() { + return estPaye; + } + + public void setEstPaye(boolean estPaye) { + this.estPaye = estPaye; + } + + public Long getCourseId() { + return courseId; + } + + public void setCourseId(Long courseId) { + this.courseId = courseId; + } + + public String getCourseNom() { + return courseNom; + } + + public void setCourseNom(String courseNom) { + this.courseNom = courseNom; + } + + public LocalDateTime getDateCourse() { + return dateCourse; + } + + public void setDateCourse(LocalDateTime dateCourse) { + this.dateCourse = dateCourse; + } + + public List getChevaux() { + return chevaux; + } + + public void setChevaux(List chevaux) { + this.chevaux = chevaux; + } + + public String getClientNomComplet() { + return clientNomComplet; + } + + public void setClientNomComplet(String clientNomComplet) { + this.clientNomComplet = clientNomComplet; + } + + public String getClientNumero() { + return clientNumero; + } + + public void setClientNumero(String clientNumero) { + this.clientNumero = clientNumero; + } + + // Méthode utilitaire de conversion depuis l'entité + public static PariResponse fromEntity(Pari pari) { + PariResponse response = new PariResponse( + pari.getId(), + "PMU-" + pari.getId(), + pari.getTypePari(), + pari.getMontantMise(), + pari.getDatePari(), + determinerStatut(pari), + pari.getGains(), + pari.isEstPaye(), + pari.getCourse().getId(), + pari.getCourse().getNom(), + pari.getCourse().getDateCourse() + ); + + response.setChevaux(pari.getChevauxJumeles().stream() + .map(ChevalDto::fromEntity) + .collect(Collectors.toList())); + + response.setClientNomComplet(pari.getClient().getPrenom() + " " + pari.getClient().getNom()); + response.setClientNumero(pari.getClient().getNumeroClient()); + + return response; + } + + private static String determinerStatut(Pari pari) { + if (pari.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant)) { + return "REMBOURSE"; + } + if (pari.isEstPaye()) { + return "GAGNANT"; + } + if (pari.getCourse().isTerminee() && !pari.isEstPaye()) { + return "PERDANT"; + } + return "EN_COURS"; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java b/src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java new file mode 100644 index 0000000..9104cd5 --- /dev/null +++ b/src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java @@ -0,0 +1,16 @@ +package com.pmumali.simple.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class ResultatCourseDto { + private Long courseId; + private List combinaisonsPayables; + private Map rapports; + private boolean tirelire; + + // Getters, setters +} diff --git a/src/main/java/com/pmumali/simple/exception/PariException.java b/src/main/java/com/pmumali/simple/exception/PariException.java new file mode 100644 index 0000000..3d72909 --- /dev/null +++ b/src/main/java/com/pmumali/simple/exception/PariException.java @@ -0,0 +1,84 @@ +package com.pmumali.simple.exception; + + +/** + * Exception métier pour les erreurs spécifiques aux paris PMU Mali + * conforme aux règles du "Jumelé Placé" + */ +public class PariException extends RuntimeException { + + private final ErrorCode errorCode; + private final String details; + + // Codes d'erreur standardisés + public enum ErrorCode { + // Article 1 - Règles de base + MISE_MINIMALE_NON_ATTEINTE("La mise minimale est de 500 FCFA"), + + // Article 2 - Limitation des enjeux + LIMITE_MISE_DEPASSEE("Limite de mise dépassée (20x500 FCFA maximum)"), + + // Article 3 - Dead Heat + CALCUL_RAPPORT_IMPOSSIBLE("Impossible de calculer les rapports pour ce Dead Heat"), + + // Article 4 - Non-partants + CHEVAL_NON_PARTANT("Pari invalide : cheval non-partant"), + + // Article 5 - Calcul des rapports + RAPPORT_INVALIDE("Rapport calculé invalide (<1.1)"), + + // Article 6 - Formules + FORMULE_INVALIDE("Formule de pari invalide"), + + // Article 8 - Cas particuliers + COURSE_ANNULEE("Course annulée - paris remboursés"), + + // Validations générales + PARI_INVALIDE("Pari invalide"), + SOLDE_INSUFFISANT("Solde insuffisant pour placer ce pari"), + CLIENT_BLOQUE("Compte client bloqué"); + + private final String message; + + ErrorCode(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + // Constructeurs + public PariException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.details = null; + } + + public PariException(ErrorCode errorCode, String details) { + super(errorCode.getMessage() + " : " + details); + this.errorCode = errorCode; + this.details = details; + } + + public PariException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); + this.errorCode = errorCode; + this.details = null; + } + + // Getters + public ErrorCode getErrorCode() { + return errorCode; + } + + public String getDetails() { + return details; + } + + // Méthode utilitaire pour construire les messages + public static String buildLimiteMiseMessage(double limite) { + return String.format("Limite de mise dépassée (max %,.0f FCFA par course selon Article 2)", limite); + } +} diff --git a/src/main/java/com/pmumali/simple/model/Cheval.java b/src/main/java/com/pmumali/simple/model/Cheval.java new file mode 100644 index 0000000..fbd5822 --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Cheval.java @@ -0,0 +1,68 @@ +package com.pmumali.simple.model; + +import jakarta.persistence.*; + +@Entity +public class Cheval { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nom; + private int numero; + + private boolean estNonPartant; + private Integer positionArrivee; + + @ManyToOne(fetch = FetchType.LAZY) + private Course course; + + + public int getNumero() { + return numero; + } + + public void setNumero(int numero) { + this.numero = numero; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getNom() { + return nom; + } + + public void setNom(String nom) { + this.nom = nom; + } + + public boolean isEstNonPartant() { + return estNonPartant; + } + + public void setEstNonPartant(boolean estNonPartant) { + this.estNonPartant = estNonPartant; + } + + public Integer getPositionArrivee() { + return positionArrivee; + } + + public void setPositionArrivee(Integer positionArrivee) { + this.positionArrivee = positionArrivee; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/model/Client.java b/src/main/java/com/pmumali/simple/model/Client.java new file mode 100644 index 0000000..7a4f85f --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Client.java @@ -0,0 +1,287 @@ +package com.pmumali.simple.model; + + + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "clients") +public class Client { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String nom; + + @Column(nullable = false, length = 50) + private String prenom; + + @Column(nullable = false, unique = true, length = 30) + private String numeroClient; + + @Column(nullable = false, unique = true, length = 50) + private String email; + + @Column(nullable = false, length = 20) + private String telephone; + + @Column(name = "date_naissance") + private LocalDate dateNaissance; + + @Column(nullable = false, length = 1) + private String sexe; // M ou F + + @Column(name = "piece_identite", nullable = false, length = 50) + private String pieceIdentite; // Numéro de CNI/Passeport + + @Column(name = "type_piece", length = 20) + private String typePieceIdentite; // CNI, PASSEPORT, PERMIS + + @Column(name = "adresse_postale", length = 100) + private String adressePostale; + + @Column(name = "code_postal", length = 10) + private String codePostal; + + @Column(length = 50) + private String ville; + + @Column(length = 50) + private String pays = "Mali"; + + @Column(name = "date_inscription", nullable = false) + private LocalDate dateInscription = LocalDate.now(); + + @Column(name = "est_verifie", nullable = false) + private boolean estVerifie = false; + + @Column(name = "est_bloque", nullable = false) + private boolean estBloque = false; + + @Column(name = "solde_compte", nullable = false) + private double soldeCompte = 0.0; + + @Column(name = "limite_mise", nullable = false) + private double limiteMise = 10000.0; // 20 x 500 FCFA (Article 2) + + @OneToMany(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true) + private List paris = new ArrayList<>(); + + @OneToMany(mappedBy = "client", cascade = CascadeType.ALL) + private List transactions = new ArrayList<>(); + + // Constructeurs + public Client() { + } + + public Client(String nom, String prenom, String numeroClient, String email, String telephone) { + this.nom = nom; + this.prenom = prenom; + this.numeroClient = numeroClient; + this.email = email; + this.telephone = telephone; + } + + // Getters et Setters + public Long getId() { + return id; + } + + public String getNom() { + return nom; + } + + public void setNom(String nom) { + this.nom = nom; + } + + public String getPrenom() { + return prenom; + } + + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + public String getNumeroClient() { + return numeroClient; + } + + public void setNumeroClient(String numeroClient) { + this.numeroClient = numeroClient; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + public LocalDate getDateNaissance() { + return dateNaissance; + } + + public void setDateNaissance(LocalDate dateNaissance) { + this.dateNaissance = dateNaissance; + } + + public String getSexe() { + return sexe; + } + + public void setSexe(String sexe) { + this.sexe = sexe; + } + + public String getPieceIdentite() { + return pieceIdentite; + } + + public void setPieceIdentite(String pieceIdentite) { + this.pieceIdentite = pieceIdentite; + } + + public String getTypePieceIdentite() { + return typePieceIdentite; + } + + public void setTypePieceIdentite(String typePieceIdentite) { + this.typePieceIdentite = typePieceIdentite; + } + + public String getAdressePostale() { + return adressePostale; + } + + public void setAdressePostale(String adressePostale) { + this.adressePostale = adressePostale; + } + + public String getCodePostal() { + return codePostal; + } + + public void setCodePostal(String codePostal) { + this.codePostal = codePostal; + } + + public String getVille() { + return ville; + } + + public void setVille(String ville) { + this.ville = ville; + } + + public String getPays() { + return pays; + } + + public void setPays(String pays) { + this.pays = pays; + } + + public LocalDate getDateInscription() { + return dateInscription; + } + + public void setDateInscription(LocalDate dateInscription) { + this.dateInscription = dateInscription; + } + + public boolean isEstVerifie() { + return estVerifie; + } + + public void setEstVerifie(boolean estVerifie) { + this.estVerifie = estVerifie; + } + + public boolean isEstBloque() { + return estBloque; + } + + public void setEstBloque(boolean estBloque) { + this.estBloque = estBloque; + } + + public double getSoldeCompte() { + return soldeCompte; + } + + public void setSoldeCompte(double soldeCompte) { + this.soldeCompte = soldeCompte; + } + + public double getLimiteMise() { + return limiteMise; + } + + public void setLimiteMise(double limiteMise) { + this.limiteMise = limiteMise; + } + + public List getParis() { + return paris; + } + + public void setParis(List paris) { + this.paris = paris; + } + + public List getTransactions() { + return transactions; + } + + public void setTransactions(List transactions) { + this.transactions = transactions; + } + + // Méthodes utilitaires + public void ajouterPari(Pari pari) { + paris.add(pari); + pari.setClient(this); + } + + public void retirerPari(Pari pari) { + paris.remove(pari); + pari.setClient(null); + } + + public void crediterCompte(double montant) { + this.soldeCompte += montant; + } + + public void debiterCompte(double montant) { + if (this.soldeCompte < montant) { + throw new IllegalStateException("Solde insuffisant"); + } + this.soldeCompte -= montant; + } + + @Override + public String toString() { + return "Client{" + + "id=" + id + + ", nom='" + nom + '\'' + + ", prenom='" + prenom + '\'' + + ", numeroClient='" + numeroClient + '\'' + + '}'; + } +} diff --git a/src/main/java/com/pmumali/simple/model/Combinaison.java b/src/main/java/com/pmumali/simple/model/Combinaison.java new file mode 100644 index 0000000..bf1c8e6 --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Combinaison.java @@ -0,0 +1,35 @@ +package com.pmumali.simple.model; + +public class Combinaison { + private final Cheval cheval1; + private final Cheval cheval2; + private int nombreMises; + + public Combinaison(Cheval cheval1, Cheval cheval2) { + this.cheval1 = cheval1; + this.cheval2 = cheval2; + } + + // Getters + public Cheval getCheval1() { return cheval1; } + public Cheval getCheval2() { return cheval2; } + public int getNombreMises() { return nombreMises; } + + public void setNombreMises(int nombreMises) { + this.nombreMises = nombreMises; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Combinaison)) return false; + Combinaison that = (Combinaison) o; + return (cheval1.equals(that.cheval1) && cheval2.equals(that.cheval2)) || + (cheval1.equals(that.cheval2) && cheval2.equals(that.cheval1)); + } + + @Override + public int hashCode() { + return cheval1.hashCode() + cheval2.hashCode(); // Ordre non important + } +} diff --git a/src/main/java/com/pmumali/simple/model/Course.java b/src/main/java/com/pmumali/simple/model/Course.java new file mode 100644 index 0000000..f957f0c --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Course.java @@ -0,0 +1,92 @@ +package com.pmumali.simple.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity + +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nom; + private LocalDateTime dateCourse; + private boolean estAnnulee; + private boolean deadHeat; + + private boolean isTerminee=false; + + public boolean isTerminee() { + return isTerminee; + } + + public void setTerminee(boolean terminee) { + isTerminee = terminee; + } + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL) + private List chevaux = new ArrayList<>(); + + @OneToMany(mappedBy = "course") + private List paris = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getNom() { + return nom; + } + + public void setNom(String nom) { + this.nom = nom; + } + + public LocalDateTime getDateCourse() { + return dateCourse; + } + + public void setDateCourse(LocalDateTime dateCourse) { + this.dateCourse = dateCourse; + } + + public boolean isEstAnnulee() { + return estAnnulee; + } + + public void setEstAnnulee(boolean estAnnulee) { + this.estAnnulee = estAnnulee; + } + + public boolean isDeadHeat() { + return deadHeat; + } + + public void setDeadHeat(boolean deadHeat) { + this.deadHeat = deadHeat; + } + + public List getChevaux() { + return chevaux; + } + + public void setChevaux(List chevaux) { + this.chevaux = chevaux; + } + + public List getParis() { + return paris; + } + + public void setParis(List paris) { + this.paris = paris; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/model/Pari.java b/src/main/java/com/pmumali/simple/model/Pari.java new file mode 100644 index 0000000..86642f6 --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Pari.java @@ -0,0 +1,108 @@ +package com.pmumali.simple.model; + +import com.pmumali.simple.model.enums.TypePari; +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class Pari { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private double montantMise; + private LocalDateTime datePari; + private boolean estPaye; + private double gains; + + @ManyToOne(fetch = FetchType.LAZY) + private Client client; + + @ManyToOne(fetch = FetchType.LAZY) + private Course course; + + @ManyToMany + @JoinTable( + name = "pari_cheval", + joinColumns = @JoinColumn(name = "pari_id"), + inverseJoinColumns = @JoinColumn(name = "cheval_id")) + private List chevauxJumeles = new ArrayList<>(); + + public TypePari getTypePari() { + return typePari; + } + + public void setTypePari(TypePari typePari) { + this.typePari = typePari; + } + + @Enumerated(EnumType.STRING) + private TypePari typePari; // JUMELEC_GAGNANT ou JUMELEC_PLACE + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public double getMontantMise() { + return montantMise; + } + + public void setMontantMise(double montantMise) { + this.montantMise = montantMise; + } + + public LocalDateTime getDatePari() { + return datePari; + } + + public void setDatePari(LocalDateTime datePari) { + this.datePari = datePari; + } + + public boolean isEstPaye() { + return estPaye; + } + + public void setEstPaye(boolean estPaye) { + this.estPaye = estPaye; + } + + public double getGains() { + return gains; + } + + public void setGains(double gains) { + this.gains = gains; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public List getChevauxJumeles() { + return chevauxJumeles; + } + + public void setChevauxJumeles(List chevauxJumeles) { + this.chevauxJumeles = chevauxJumeles; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/model/ResultatCourse.java b/src/main/java/com/pmumali/simple/model/ResultatCourse.java new file mode 100644 index 0000000..2854c87 --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/ResultatCourse.java @@ -0,0 +1,192 @@ +package com.pmumali.simple.model; + + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.*; + +@Entity +@Table(name = "resultats_courses") +public class ResultatCourse { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "course_id", nullable = false, unique = true) + private Course course; + + @Column(name = "date_publication", nullable = false) + private LocalDateTime datePublication = LocalDateTime.now(); + + @Column(name = "est_officiel", nullable = false) + private boolean estOfficiel = false; + + @Column(name = "masse_a_partager", nullable = false) + private double masseAPartager; + + @Column(name = "tirelire_active", nullable = false) + private boolean tirelireActive = false; + + @Column(name = "montant_tirelire") + private Double montantTirelire; + + @ElementCollection + @CollectionTable(name = "resultats_combinaisons", joinColumns = @JoinColumn(name = "resultat_id")) + @MapKeyColumn(name = "combinaison") + @Column(name = "rapport") + private Map rapports = new HashMap<>(); + + @ElementCollection + @CollectionTable(name = "resultats_non_partants", joinColumns = @JoinColumn(name = "resultat_id")) + @Column(name = "cheval_id") + private Set chevauxNonPartants = new HashSet<>(); + + @Enumerated(EnumType.STRING) + @Column(name = "type_resultat", nullable = false) + private TypeResultat typeResultat; + + @Version + private Long version; + + // Enumération pour les types de résultats + public enum TypeResultat { + NORMAL, + DEAD_HEAT_PREMIERS, + DEAD_HEAT_DEUXIEMES, + DEAD_HEAT_TROISIEMES, + COURSE_ANNULEE + } + + // Constructeurs + public ResultatCourse() {} + + public ResultatCourse(Course course) { + this.course = course; + } + + // Getters et Setters + public Long getId() { + return id; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public LocalDateTime getDatePublication() { + return datePublication; + } + + public void setDatePublication(LocalDateTime datePublication) { + this.datePublication = datePublication; + } + + public boolean isEstOfficiel() { + return estOfficiel; + } + + public void setEstOfficiel(boolean estOfficiel) { + this.estOfficiel = estOfficiel; + } + + public double getMasseAPartager() { + return masseAPartager; + } + + public void setMasseAPartager(double masseAPartager) { + this.masseAPartager = masseAPartager; + } + + public boolean isTirelireActive() { + return tirelireActive; + } + + public void setTirelireActive(boolean tirelireActive) { + this.tirelireActive = tirelireActive; + } + + public Double getMontantTirelire() { + return montantTirelire; + } + + public void setMontantTirelire(Double montantTirelire) { + this.montantTirelire = montantTirelire; + } + + public Map getRapports() { + return rapports; + } + + public void setRapports(Map rapports) { + this.rapports = rapports; + } + + public Set getChevauxNonPartants() { + return chevauxNonPartants; + } + + public void setChevauxNonPartants(Set chevauxNonPartants) { + this.chevauxNonPartants = chevauxNonPartants; + } + + public TypeResultat getTypeResultat() { + return typeResultat; + } + + public void setTypeResultat(TypeResultat typeResultat) { + this.typeResultat = typeResultat; + } + + public Long getVersion() { + return version; + } + + // Méthodes métiers + public void ajouterRapport(String combinaison, double rapport) { + this.rapports.put(combinaison, rapport); + } + + public void marquerNonPartant(Long chevalId) { + this.chevauxNonPartants.add(chevalId); + } + + public boolean estCombinaisonPayable(String combinaison) { + return this.rapports.containsKey(combinaison); + } + + public void activerTirelire(double montant) { + this.tirelireActive = true; + this.montantTirelire = montant; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ResultatCourse)) return false; + ResultatCourse that = (ResultatCourse) o; + return Objects.equals(id, that.id) && + Objects.equals(course, that.course); + } + + @Override + public int hashCode() { + return Objects.hash(id, course); + } + + @Override + public String toString() { + return "ResultatCourse{" + + "id=" + id + + ", course=" + course.getId() + + ", typeResultat=" + typeResultat + + ", rapports=" + rapports.size() + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/model/Transaction.java b/src/main/java/com/pmumali/simple/model/Transaction.java new file mode 100644 index 0000000..a56eacf --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/Transaction.java @@ -0,0 +1,33 @@ +package com.pmumali.simple.model; + + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "transactions") +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private double montant; + + @Column(nullable = false) + private LocalDateTime dateHeure = LocalDateTime.now(); + + @Column(nullable = false, length = 20) + private String type; // DÉPÔT, RETRAIT, GAIN, REMBOURSEMENT + + @Column(length = 100) + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "client_id") + private Client client; + + // Getters, setters et constructeurs +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/simple/model/enums/TypePari.java b/src/main/java/com/pmumali/simple/model/enums/TypePari.java new file mode 100644 index 0000000..cb2a077 --- /dev/null +++ b/src/main/java/com/pmumali/simple/model/enums/TypePari.java @@ -0,0 +1,6 @@ +package com.pmumali.simple.model.enums; + +public enum TypePari { + JUMELEC_GAGNANT, + JUMELEC_PLACE +} diff --git a/src/main/java/com/pmumali/simple/repository/ChevalRepository.java b/src/main/java/com/pmumali/simple/repository/ChevalRepository.java new file mode 100644 index 0000000..ba6f510 --- /dev/null +++ b/src/main/java/com/pmumali/simple/repository/ChevalRepository.java @@ -0,0 +1,7 @@ +package com.pmumali.simple.repository; + +import com.pmumali.simple.model.Cheval; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChevalRepository extends JpaRepository { +} diff --git a/src/main/java/com/pmumali/simple/repository/CourseRepository.java b/src/main/java/com/pmumali/simple/repository/CourseRepository.java new file mode 100644 index 0000000..1b74d4f --- /dev/null +++ b/src/main/java/com/pmumali/simple/repository/CourseRepository.java @@ -0,0 +1,7 @@ +package com.pmumali.simple.repository; + +import com.pmumali.simple.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CourseRepository extends JpaRepository { +} diff --git a/src/main/java/com/pmumali/simple/repository/PariRepository.java b/src/main/java/com/pmumali/simple/repository/PariRepository.java new file mode 100644 index 0000000..fe26e48 --- /dev/null +++ b/src/main/java/com/pmumali/simple/repository/PariRepository.java @@ -0,0 +1,72 @@ +package com.pmumali.simple.repository; + +import com.pmumali.simple.model.Pari; +import com.pmumali.simple.model.Client; +import com.pmumali.simple.model.Course; +import com.pmumali.simple.model.enums.TypePari; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public interface PariRepository extends JpaRepository { + +double sumByClientAndCourse(Long clientID, Long courseId); + + // Trouver tous les paris d'un client + List findByClient(Client client); + + // Trouver les paris par course + List findByCourse(Course course); + + // Trouver les paris par type (JUMELEC_PLACE ou JUMELEC_GAGNANT) + List findByTypePari(TypePari typePari); + + // Somme des mises d'un client pour une course (Article 2 - Limitation des enjeux) + @Query("SELECT COALESCE(SUM(p.montantMise), 0) FROM Pari p WHERE p.client = :client AND p.course = :course") + double sumMisesByClientAndCourse(@Param("client") Client client, @Param("course") Course course); + + // Trouver les paris gagnants pour une combinaison donnée + @Query("SELECT p FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course " + + "AND c.id IN (:cheval1Id, :cheval2Id) GROUP BY p HAVING COUNT(c) = 2") + List findParisGagnantsForCombinaison( + @Param("course") Course course, + @Param("cheval1Id") Long cheval1Id, + @Param("cheval2Id") Long cheval2Id + ); + + // Nombre de mises pour une combinaison spécifique + @Query("SELECT COUNT(p) FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course " + + "AND c.id IN (:cheval1Id, :cheval2Id) GROUP BY p HAVING COUNT(c) = 2") + long countMisesForCombinaison( + @Param("course") Course course, + @Param("cheval1Id") Long cheval1Id, + @Param("cheval2Id") Long cheval2Id + ); + + // Trouver les paris contenant un cheval non-partant (Article 4) + @Query("SELECT DISTINCT p FROM Pari p JOIN p.chevauxJumeles c WHERE p.course = :course AND c.estNonPartant = true") + List findParisAvecNonPartants(@Param("course") Course course); + + // Statistiques pour le dashboard + @Query("SELECT NEW map(p.typePari as type, COUNT(p) as nombre, SUM(p.montantMise) as totalMises) " + + "FROM Pari p WHERE p.datePari BETWEEN :start AND :end GROUP BY p.typePari") + List> getStatistiquesParType( + @Param("start") LocalDateTime startDate, + @Param("end") LocalDateTime endDate + ); + + // Trouver les paris non payés pour une course + List findByCourseAndEstPayeFalse(Course course); + + // Méthode optimisée pour le calcul des rapports + @Query("SELECT NEW map(c1.id as cheval1Id, c2.id as cheval2Id, COUNT(p) as mises) " + + "FROM Pari p JOIN p.chevauxJumeles c1 JOIN p.chevauxJumeles c2 " + + "WHERE p.course = :course AND c1.id < c2.id " + + "GROUP BY c1.id, c2.id") + List> getMisesParCombinaison(@Param("course") Course course); + + + } diff --git a/src/main/java/com/pmumali/simple/service/CalculRapportService.java b/src/main/java/com/pmumali/simple/service/CalculRapportService.java new file mode 100644 index 0000000..0f25399 --- /dev/null +++ b/src/main/java/com/pmumali/simple/service/CalculRapportService.java @@ -0,0 +1,138 @@ +package com.pmumali.simple.service; + +import com.pmumali.simple.model.Cheval; +import com.pmumali.simple.model.Combinaison; +import com.pmumali.simple.model.Course; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CalculRapportService { + + public Map calculerRapports( + List combinaisonsPayables, + double masseAPartager) { + + Map rapports = new HashMap<>(); + + if (combinaisonsPayables.size() == 1) { + // Cas arrivée normale + double rapport = masseAPartager / combinaisonsPayables.get(0).getNombreMises(); + rapports.put(combinaisonsPayables.get(0), Math.max(1.1, rapport)); + } else { + // Cas Dead-Heat + double beneficeParCombinaison = masseAPartager / combinaisonsPayables.size(); + + for (Combinaison combinaison : combinaisonsPayables) { + double rapport = beneficeParCombinaison / combinaison.getNombreMises(); + rapports.put(combinaison, Math.max(1.1, rapport)); + } + } + + return rapports; + } + + /** + * Vérifie si une combinaison est gagnante selon les positions d'arrivée + */ + public boolean isCombinaisonGagnante(Combinaison combinaison, List classement) { + if (classement.size() < 2) return false; + + Cheval c1 = combinaison.getCheval1(); + Cheval c2 = combinaison.getCheval2(); + + // Les deux chevaux doivent être dans les 2 premiers (ordre quelconque) + return classement.subList(0, 2).contains(c1) && + classement.subList(0, 2).contains(c2); + } + + /** + * Calcule le nombre de mises pour une combinaison donnée + */ + public long compterMisesCombinaison(Course course, Combinaison combinaison) { + return course.getParis().stream() + .filter(p -> p.getChevauxJumeles().containsAll( + List.of(combinaison.getCheval1(), combinaison.getCheval2()) + )) + .count(); + } + + + public Map calculerRapportsJumelePlace( + List combinaisonsPayables, + double masseAPartager) { + + Map rapports = new HashMap<>(); + + if (combinaisonsPayables.isEmpty()) { + return rapports; + } + + // Article 5a: Arrivée normale - division en 3 parts égales + if (!combinaisonsPayables.get(0).getCourse().isDeadHeat()) { + double part = masseAPartager / 3; + + for (Combinaison combinaison : combinaisonsPayables) { + double rapport = part / compterMisesCombinaison(combinaison); + rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport)); + } + + return rapports; + } + + // Article 5b: Dead-Heat + Course course = combinaisonsPayables.get(0).getCourse(); + Map> parPosition = course.getChevaux().stream() + .filter(c -> !c.isEstNonPartant()) + .collect(Collectors.groupingBy(Cheval::getPositionArrivee)); + + List premiers = parPosition.getOrDefault(1, Collections.emptyList()); + List deuxiemes = parPosition.getOrDefault(2, Collections.emptyList()); + List troisiemes = parPosition.getOrDefault(3, Collections.emptyList()); + + // Article 5b1: Dead-Heat 3+ premiers + if (premiers.size() >= 3) { + double part = masseAPartager / combinaisonsPayables.size(); + for (Combinaison combinaison : combinaisonsPayables) { + double rapport = part / compterMisesCombinaison(combinaison); + rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport)); + } + return rapports; + } + + // Article 5b2: Dead-Heat 2 premiers + 1+ troisième + if (premiers.size() == 2 && !troisiemes.isEmpty()) { + // Répartition en 3 tiers + double tiers = masseAPartager / 3; + + // 1er tiers: combinaison des 2 premiers + Combinaison combinaisonPremiers = new Combinaison(premiers.get(0), premiers.get(1)); + double rapportPremiers = tiers / compterMisesCombinaison(combinaisonPremiers); + rapports.put(combinaisonPremiers, Math.max(RAPPORT_MINIMUM, rapportPremiers)); + + // 2ème tiers: premier1 avec troisièmes + double partParCombinaison = tiers / troisiemes.size(); + for (Cheval troisieme : troisiemes) { + Combinaison combinaison = new Combinaison(premiers.get(0), troisieme); + double rapport = partParCombinaison / compterMisesCombinaison(combinaison); + rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport)); + } + + // 3ème tiers: premier2 avec troisièmes + for (Cheval troisieme : troisiemes) { + Combinaison combinaison = new Combinaison(premiers.get(1), troisieme); + double rapport = partParCombinaison / compterMisesCombinaison(combinaison); + rapports.put(combinaison, Math.max(RAPPORT_MINIMUM, rapport)); + } + + return rapports; + } + + // ... autres cas Dead-Heat (implémenter de manière similaire) + + return rapports; + } +} diff --git a/src/main/java/com/pmumali/simple/service/FormulaireService.java b/src/main/java/com/pmumali/simple/service/FormulaireService.java new file mode 100644 index 0000000..357fcd0 --- /dev/null +++ b/src/main/java/com/pmumali/simple/service/FormulaireService.java @@ -0,0 +1,48 @@ +package com.pmumali.simple.service; + +import com.pmumali.simple.model.Cheval; +import com.pmumali.simple.model.Combinaison; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class FormulaireService { + + public double calculerCoutFormule(int nombreChevaux, boolean formuleComplete) { + // Article 7: Tableaux des combinaisons + int nbCombinaisons = formuleComplete ? + nombreChevaux * (nombreChevaux - 1) / 2 : + nombreChevaux; + + return nbCombinaisons * 500; // 500 FCFA par combinaison + } + + public List genererCombinaisonsFormule( + List chevauxSelectionnes, + boolean formuleComplete) { + + List combinaisons = new ArrayList<>(); + + if (formuleComplete) { + // Toutes les combinaisons 2 à 2 + for (int i = 0; i < chevauxSelectionnes.size(); i++) { + for (int j = i + 1; j < chevauxSelectionnes.size(); j++) { + combinaisons.add(new Combinaison( + chevauxSelectionnes.get(i), + chevauxSelectionnes.get(j) + )); + } + } + } else { + // Formule simplifiée (champ total/partiel) + Cheval base = chevauxSelectionnes.get(0); + for (int i = 1; i < chevauxSelectionnes.size(); i++) { + combinaisons.add(new Combinaison(base, chevauxSelectionnes.get(i))); + } + } + + return combinaisons; + } +} diff --git a/src/main/java/com/pmumali/simple/service/PariService.java b/src/main/java/com/pmumali/simple/service/PariService.java new file mode 100644 index 0000000..4386fc0 --- /dev/null +++ b/src/main/java/com/pmumali/simple/service/PariService.java @@ -0,0 +1,309 @@ +package com.pmumali.simple.service; + +import com.pmumali.simple.dto.CombinaisonDto; +import com.pmumali.model.*; +import com.pmumali.simple.repository.ChevalRepository; +import com.pmumali.simple.repository.CourseRepository; +import com.pmumali.simple.repository.PariRepository; +import com.pmumali.simple.model.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class PariService { + + @Autowired + private PariRepository pariRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private ChevalRepository chevalRepository; + + + public Pari placerPariJumele(Long clientId, Long courseId, + List chevauxIds, double montant) { + // Vérification mise minimum + if (montant < 500) { + throw new IllegalArgumentException("La mise minimale est de 500 FCFA"); + } + + // Vérification limite de paris (20x mise min = 10 000 FCFA) + double totalParisClient = pariRepository + .sumByClientAndCourse(clientId, courseId); + + if (totalParisClient + montant > 10000) { + throw new IllegalArgumentException("Limite de mise dépassée"); + } + + // Création du pari + Pari pari = new Pari(); + // ... initialisation + + return pariRepository.save(pari); + } + + public ResultatCourse calculerResultats(Long courseId) throws Exception { + Course course = courseRepository.findById(courseId) + .orElseThrow(() -> new Exception("Course non trouvée")); + + // Implémentation des règles de calcul + return calculerResultatsSelonReglement(course); + } + + private ResultatCourse calculerResultatsSelonReglement(Course course) { + ResultatCourse resultat = new ResultatCourse(); + + // 1. Vérifier les non-partants (Article 4) + List nonPartants = course.getChevaux().stream() + .filter(Cheval::isEstNonPartant) + .collect(Collectors.toList()); + + // 2. Calcul selon Dead-Heat (Article 3) + if (course.isDeadHeat()) { + calculerDeadHeat(resultat, course); + } else { + calculerArriveeNormale(resultat, course); + } + + // 3. Appliquer tirelire si nécessaire (Article 9) + if (resultat.getCombinaisonsPayables().isEmpty()) { + resultat.setTirelire(true); + } + + return resultat; + } + + + /** + * Détermine les combinaisons payables selon les règles du PMU Mali + */ + private List determinerCombinaisonsPayables(Course course) { + List chevauxArrives = course.getChevaux().stream() + .filter(c -> !c.isEstNonPartant()) + .sorted(Comparator.comparingInt(Cheval::getPositionArrivee)) + .collect(Collectors.toList()); + + // Article 4: Remboursement si non-partant + if (course.getChevaux().stream().anyMatch(Cheval::isEstNonPartant)) { + return Collections.emptyList(); + } + + // Article 3: Gestion Dead-Heat + if (course.isDeadHeat()) { + return calculerCombinaisonsDeadHeat(chevauxArrives); + } + + // Article 5: Arrivée normale + return calculerCombinaisonsNormales(chevauxArrives); + } + + /** + * Implémentation de l'article 3 - Cas Dead-Heat + */ + private List calculerCombinaisonsDeadHeat(List chevauxArrives) { + List combinaisons = new ArrayList<>(); + + // Groupes par position d'arrivée + Map> parPosition = chevauxArrives.stream() + .collect(Collectors.groupingBy(Cheval::getPositionArrivee)); + + List premiers = parPosition.getOrDefault(1, Collections.emptyList()); + List deuxiemes = parPosition.getOrDefault(2, Collections.emptyList()); + + // Cas Dead-Heat premier place (Article 3a) + if (premiers.size() >= 2) { + for (int i = 0; i < premiers.size(); i++) { + for (int j = i + 1; j < premiers.size(); j++) { + combinaisons.add(new Combinaison(premiers.get(i), premiers.get(j))); + } + } + } + // Cas Dead-Heat deuxième place (Article 3b) + else if (deuxiemes.size() >= 2) { + Cheval premier = premiers.get(0); + for (Cheval deuxieme : deuxiemes) { + combinaisons.add(new Combinaison(premier, deuxieme)); + } + } + + return combinaisons; + } + + /** + * Implémentation de l'article 5 - Arrivée normale + */ + private List calculerCombinaisonsNormales(List chevauxArrives) { + if (chevauxArrives.size() < 2) { + return Collections.emptyList(); + } + + Cheval premier = chevauxArrives.get(0); + Cheval deuxieme = chevauxArrives.get(1); + + return List.of(new Combinaison(premier, deuxieme)); + } + + /** + * Calcule la masse à partager selon l'article 5 + */ + private double calculerMasseAPartager(Course course, List combinaisonsPayables) { + double totalEnjeux = course.getParis().stream() + .mapToDouble(Pari::getMontantMise) + .sum(); + + double montantRembourse = course.getParis().stream() + .filter(p -> p.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant)) + .mapToDouble(Pari::getMontantMise) + .sum(); + + // Article 5: MAP = RNET - MREMB - PRELEV + double prelevementsLegaux = totalEnjeux * 0.15; // Exemple: 15% de prélèvement + return totalEnjeux - montantRembourse - prelevementsLegaux; + } + + /** + * Convertit les combinaisons en DTO pour la réponse API + */ + private List convertToDto(List combinaisons) { + return combinaisons.stream() + .map(c -> new CombinaisonDto( + c.getCheval1().getNom(), + c.getCheval2().getNom(), + c.getNombreMises() + )) + .collect(Collectors.toList()); + } + + /** + * Convertit les rapports en format lisible + */ + private Map convertRapports(Map rapports) { + return rapports.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().getCheval1().getNom() + "-" + e.getKey().getCheval2().getNom(), + Map.Entry::getValue + )); + } + + private List determinerCombinaisonsPayablesJumelePlace(Course course) { + List chevauxArrives = course.getChevaux().stream() + .filter(c -> !c.isEstNonPartant()) + .sorted(Comparator.comparingInt(Cheval::getPositionArrivee)) + .collect(Collectors.toList()); + + // Article 4: Remboursement si non-partant + if (course.getChevaux().stream().anyMatch(Cheval::isEstNonPartant)) { + return Collections.emptyList(); + } + + // Article 8: Moins de 3 chevaux arrivés + if (chevauxArrives.size() < 3) { + return Collections.emptyList(); + } + + // Article 3: Gestion Dead-Heat + if (course.isDeadHeat()) { + return calculerCombinaisonsDeadHeatJumelePlace(chevauxArrives); + } + + // Cas normal - Article 1 + return calculerCombinaisonsNormalesJumelePlace(chevauxArrives); + } + + private List calculerCombinaisonsNormalesJumelePlace(List chevauxArrives) { + List combinaisons = new ArrayList<>(); + Cheval premier = chevauxArrives.get(0); + Cheval deuxieme = chevauxArrives.get(1); + Cheval troisieme = chevauxArrives.get(2); + + // Toutes les combinaisons 2 parmi 3 + combinaisons.add(new Combinaison(premier, deuxieme)); + combinaisons.add(new Combinaison(premier, troisieme)); + combinaisons.add(new Combinaison(deuxieme, troisieme)); + + return combinaisons; + } + + private List calculerCombinaisonsDeadHeatJumelePlace(List chevauxArrives) { + List combinaisons = new ArrayList<>(); + Map> parPosition = chevauxArrives.stream() + .collect(Collectors.groupingBy(Cheval::getPositionArrivee)); + + List premiers = parPosition.getOrDefault(1, Collections.emptyList()); + List deuxiemes = parPosition.getOrDefault(2, Collections.emptyList()); + List troisiemes = parPosition.getOrDefault(3, Collections.emptyList()); + + // Article 3a: Dead-Heat à la première place (3+ chevaux) + if (premiers.size() >= 3) { + for (int i = 0; i < premiers.size(); i++) { + for (int j = i + 1; j < premiers.size(); j++) { + combinaisons.add(new Combinaison(premiers.get(i), premiers.get(j))); + } + } + return combinaisons; + } + + // Article 3b: Dead-Heat 2 premiers + 1+ troisième + if (premiers.size() == 2 && !troisiemes.isEmpty()) { + // Combinaison des deux premiers + combinaisons.add(new Combinaison(premiers.get(0), premiers.get(1))); + + // Combinaisons premier1 avec troisièmes + for (Cheval troisieme : troisiemes) { + combinaisons.add(new Combinaison(premiers.get(0), troisieme)); + } + + // Combinaisons premier2 avec troisièmes + for (Cheval troisieme : troisiemes) { + combinaisons.add(new Combinaison(premiers.get(1), troisieme)); + } + return combinaisons; + } + + // Article 3c: Dead-Heat à la deuxième place + if (!deuxiemes.isEmpty() && deuxiemes.size() >= 2) { + Cheval premier = premiers.get(0); + + // Combinaisons premier avec deuxièmes + for (Cheval deuxieme : deuxiemes) { + combinaisons.add(new Combinaison(premier, deuxieme)); + } + + // Combinaisons deuxièmes entre eux + for (int i = 0; i < deuxiemes.size(); i++) { + for (int j = i + 1; j < deuxiemes.size(); j++) { + combinaisons.add(new Combinaison(deuxiemes.get(i), deuxiemes.get(j))); + } + } + return combinaisons; + } + + // Article 3d: Dead-Heat à la troisième place + if (!troisiemes.isEmpty() && troisiemes.size() >= 2) { + Cheval premier = premiers.get(0); + Cheval deuxieme = deuxiemes.get(0); + + // Combinaison premier-deuxième + combinaisons.add(new Combinaison(premier, deuxieme)); + + // Combinaisons premier avec troisièmes + for (Cheval troisieme : troisiemes) { + combinaisons.add(new Combinaison(premier, troisieme)); + } + + // Combinaisons deuxième avec troisièmes + for (Cheval troisieme : troisiemes) { + combinaisons.add(new Combinaison(deuxieme, troisieme)); + } + return combinaisons; + } + + return Collections.emptyList(); + } + +} diff --git a/src/main/java/com/pmumali/simple/service/PariServiceTest.java b/src/main/java/com/pmumali/simple/service/PariServiceTest.java new file mode 100644 index 0000000..e09c5c0 --- /dev/null +++ b/src/main/java/com/pmumali/simple/service/PariServiceTest.java @@ -0,0 +1,43 @@ +package com.pmumali.simple.service; + +import com.pmumali.simple.model.Cheval; +import com.pmumali.simple.model.Combinaison; +import com.pmumali.simple.model.Course; + +import java.util.List; + +public class PariServiceTest { + + @Test + public void testCombinaisonsPayables_Normal() { + Course course = new Course(); + course.setDeadHeat(false); + + Cheval c1 = new Cheval(); c1.setPositionArrivee(1); + Cheval c2 = new Cheval(); c2.setPositionArrivee(2); + Cheval c3 = new Cheval(); c3.setPositionArrivee(3); + course.setChevaux(List.of(c1, c2, c3)); + + List result = pariService.determinerCombinaisonsPayablesJumelePlace(course); + + assertEquals(3, result.size()); + assertTrue(result.contains(new Combinaison(c1, c2))); + assertTrue(result.contains(new Combinaison(c1, c3))); + assertTrue(result.contains(new Combinaison(c2, c3))); + } + + @Test + public void testDeadHeat_TroisPremiers() { + Course course = new Course(); + course.setDeadHeat(true); + + Cheval c1 = new Cheval(); c1.setPositionArrivee(1); + Cheval c2 = new Cheval(); c2.setPositionArrivee(1); + Cheval c3 = new Cheval(); c3.setPositionArrivee(1); + course.setChevaux(List.of(c1, c2, c3)); + + List result = pariService.determinerCombinaisonsPayablesJumelePlace(course); + + assertEquals(3, result.size()); // C(3,2) = 3 combinaisons + } +} diff --git a/src/main/java/com/pmumali/simple/service/ValidationPariService.java b/src/main/java/com/pmumali/simple/service/ValidationPariService.java new file mode 100644 index 0000000..b3212ee --- /dev/null +++ b/src/main/java/com/pmumali/simple/service/ValidationPariService.java @@ -0,0 +1,116 @@ +package com.pmumali.simple.service; + +import com.pmumali.simple.dto.PariRequest; +import com.pmumali.simple.exception.PariException; +import com.pmumali.simple.model.Cheval; +import com.pmumali.simple.model.Client; +import com.pmumali.simple.model.Course; +import com.pmumali.simple.model.Pari; +import com.pmumali.simple.model.enums.TypePari; +import com.pmumali.simple.repository.PariRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ValidationPariService { + @Autowired + private PariRepository pariRepository; + + public void validerPari(Pari pari) { + // Article 2: Limitation des enjeux + validerLimiteEnjeux(pari); + + // Article 4: Chevaux non-partants + validerChevauxPartants(pari.getCourse(), pari.getChevauxJumeles()); + + // Article 6: Formules combinées + validerFormule(pari); + } + + private void validerLimiteEnjeux(Pari pari) { + double totalMises = pariRepository + .sumByClientAndCourse(pari.getClient().getId(), pari.getCourse().getId()); + + if (totalMises + pari.getMontantMise() > 10000) { + throw new PariException(PariException.ErrorCode.LIMITE_MISE_DEPASSEE, "Limite de mise dépassée (20x500 FCFA maximum)"); + } + } + + /** + * Valide la requête de pari selon les règles métier + */ + public void validerPariRequest(PariRequest request) { + if (request.getMontantMise() < 500) { + throw new PariException(PariException.ErrorCode.MISE_MINIMALE_NON_ATTEINTE,"La mise minimale est de 500 FCFA (Article 1)"); + } + + if (request.getChevauxIds() == null || request.getChevauxIds().size() != 2) { + throw new PariException(PariException.ErrorCode.PARI_INVALIDE,"Un pari Jumelé Gagnant doit porter sur exactement 2 chevaux"); + } + } + + /** + * Valide le pari selon toutes les règles du règlement + */ + public void validerPari(Client client, Course course, List chevaux, double montantMise) { + // Article 2: Limitation des enjeux + validerLimiteEnjeux(client, course, montantMise); + + // Article 4: Chevaux non-partants + validerChevauxPartants(course, chevaux); + + // Article 10: Course non annulée + if (course.isEstAnnulee()) { + throw new PariException(PariException.ErrorCode.COURSE_ANNULEE,"Course annulée - tous les paris seront remboursés (Article 8)"); + } + + // Vérifie que les chevaux appartiennent bien à la course + if (chevaux.stream().anyMatch(c -> !c.getCourse().equals(course))) { + throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT,"Un ou plusieurs chevaux ne font pas partie de cette course"); + } + } + + /** + * Implémentation de l'article 2 - Limitation des enjeux + */ + private void validerLimiteEnjeux(Client client, Course course, double nouvelleMise) { + double totalMises = pariRepository.sumByClientAndCourse(client.getId(), course.getId()); + double limite = 20 * 500; // 20 fois la mise de base (500 FCFA) + + if (totalMises + nouvelleMise > limite) { + throw new PariException( PariException.ErrorCode.LIMITE_MISE_DEPASSEE, + String.format("Limite de mise dépassée (max %,.0f FCFA par course selon Article 2)", limite) + ); + } + } + + /** + * Implémentation de l'article 4 - Chevaux non-partants + */ + private void validerChevauxPartants(Course course, List chevaux) { + if (chevaux.stream().anyMatch(Cheval::isEstNonPartant)) { + throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT, + "Pari non valide : un ou plusieurs chevaux sont non-partants (Article 4)" + ); + } + } + + public void validerPariJumelePlace(Pari pari) { + // Article 1: Vérification que c'est bien un Jumelé Placé + if (pari.getTypePari() != TypePari.JUMELEC_PLACE) { + throw new PariException(PariException.ErrorCode.FORMULE_INVALIDE ,"Ce n'est pas un pari Jumelé Placé"); + } + + // Article 2: Limitation des enjeux (identique) + validerLimiteEnjeux(pari.getClient(), pari.getCourse(), pari.getMontantMise()); + + // Article 4: Chevaux non-partants + if (pari.getChevauxJumeles().stream().anyMatch(Cheval::isEstNonPartant)) { + throw new PariException(PariException.ErrorCode.CHEVAL_NON_PARTANT,"Pari non valide: cheval non-partant (Article 4)"); + } + } + + +} diff --git a/src/main/java/com/pmumali/trio/controller/ParisTrioController.java b/src/main/java/com/pmumali/trio/controller/ParisTrioController.java new file mode 100644 index 0000000..7e752d4 --- /dev/null +++ b/src/main/java/com/pmumali/trio/controller/ParisTrioController.java @@ -0,0 +1,25 @@ +package com.pmumali.trio.controller; + +import com.pmumali.trio.dto.ParisTrioDto; +import com.pmumali.trio.model.ParisTrio; +import com.pmumali.trio.service.ParisTrioService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/paris-trio") +public class ParisTrioController { + @Autowired + private ParisTrioService parisTrioService; + + @PostMapping + public ResponseEntity enregistrerParis(@RequestBody ParisTrioDto parisDto) { + try { + ParisTrio paris = parisTrioService.enregistrerParis(parisDto); + return ResponseEntity.ok(paris); + } catch (Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/controller/ResultatCourseController.java b/src/main/java/com/pmumali/trio/controller/ResultatCourseController.java new file mode 100644 index 0000000..f326f3b --- /dev/null +++ b/src/main/java/com/pmumali/trio/controller/ResultatCourseController.java @@ -0,0 +1,24 @@ +package com.pmumali.trio.controller; + +import com.pmumali.trio.dto.ResultatCourseDto; +import com.pmumali.trio.service.ResultatCourseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/resultats") +public class ResultatCourseController { + @Autowired + private ResultatCourseService resultatCourseService; + + @PostMapping + public ResponseEntity enregistrerResultat(@RequestBody ResultatCourseDto resultatDto) { + try { + resultatCourseService.traiterResultatCourse(resultatDto); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/dto/ParisTrioDto.java b/src/main/java/com/pmumali/trio/dto/ParisTrioDto.java new file mode 100644 index 0000000..f700594 --- /dev/null +++ b/src/main/java/com/pmumali/trio/dto/ParisTrioDto.java @@ -0,0 +1,15 @@ +package com.pmumali.trio.dto; + +import com.pmumali.trio.model.ParisTrio.TypeFormule; +import lombok.Data; + +@Data +public class ParisTrioDto { + private Long idCourse; + private Long idCheval1; + private Long idCheval2; + private Long idCheval3; + private double mise; + private String idParieur; + private TypeFormule typeFormule; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/dto/ResultatCourseDto.java b/src/main/java/com/pmumali/trio/dto/ResultatCourseDto.java new file mode 100644 index 0000000..3d4e296 --- /dev/null +++ b/src/main/java/com/pmumali/trio/dto/ResultatCourseDto.java @@ -0,0 +1,22 @@ +package com.pmumali.trio.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ResultatCourseDto { + private Long idCourse; + private Long idChevalPremier; + private Long idChevalDeuxieme; + private Long idChevalTroisieme; + private List chevauxDeadHeatPremierePlace; + private List chevauxDeadHeatDeuxiemePlace; + private List chevauxDeadHeatTroisiemePlace; + + public boolean hasDeadHeat() { + return !chevauxDeadHeatPremierePlace.isEmpty() || + !chevauxDeadHeatDeuxiemePlace.isEmpty() || + !chevauxDeadHeatTroisiemePlace.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/exception/ChevalNonPartantException.java b/src/main/java/com/pmumali/trio/exception/ChevalNonPartantException.java new file mode 100644 index 0000000..9feba09 --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/ChevalNonPartantException.java @@ -0,0 +1,7 @@ +package com.pmumali.trio.exception; + +public class ChevalNonPartantException extends ParisTrioInvalideException { + public ChevalNonPartantException(String nomCheval) { + super("Le cheval " + nomCheval + " est non partant"); + } +} diff --git a/src/main/java/com/pmumali/trio/exception/CourseDejaTermineeException.java b/src/main/java/com/pmumali/trio/exception/CourseDejaTermineeException.java new file mode 100644 index 0000000..2947c1b --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/CourseDejaTermineeException.java @@ -0,0 +1,7 @@ +package com.pmumali.trio.exception; + +public class CourseDejaTermineeException extends ParisTrioInvalideException { + public CourseDejaTermineeException(Long idCourse) { + super("La course " + idCourse + " est déjà terminée"); + } +} diff --git a/src/main/java/com/pmumali/trio/exception/CourseInvalideException.java b/src/main/java/com/pmumali/trio/exception/CourseInvalideException.java new file mode 100644 index 0000000..34af1cc --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/CourseInvalideException.java @@ -0,0 +1,14 @@ +// CourseInvalideException.java +package com.pmumali.trio.exception; + +import com.pmumali.jumeleordre.exception.JumeleOrdreException; + +public class CourseInvalideException extends JumeleOrdreException { + public CourseInvalideException(String message) { + super(message); + } + + public CourseInvalideException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/exception/LimiteMiseDepasseeException.java b/src/main/java/com/pmumali/trio/exception/LimiteMiseDepasseeException.java new file mode 100644 index 0000000..697a6fc --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/LimiteMiseDepasseeException.java @@ -0,0 +1,21 @@ +package com.pmumali.trio.exception; + +public class LimiteMiseDepasseeException extends ParisTrioInvalideException { + 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; + } +} diff --git a/src/main/java/com/pmumali/trio/exception/NombreChevauxInvalideException.java b/src/main/java/com/pmumali/trio/exception/NombreChevauxInvalideException.java new file mode 100644 index 0000000..03fbf6f --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/NombreChevauxInvalideException.java @@ -0,0 +1,7 @@ +package com.pmumali.trio.exception; + +public class NombreChevauxInvalideException extends ParisTrioInvalideException { + public NombreChevauxInvalideException(int nombre) { + super("Nombre de chevaux invalide: " + nombre); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/exception/ParisInvalideException.java b/src/main/java/com/pmumali/trio/exception/ParisInvalideException.java new file mode 100644 index 0000000..1e84b10 --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/ParisInvalideException.java @@ -0,0 +1,14 @@ +// ParisInvalideException.java +package com.pmumali.trio.exception; + +import com.pmumali.jumeleordre.exception.JumeleOrdreException; + +public class ParisInvalideException extends JumeleOrdreException { + public ParisInvalideException(String message) { + super(message); + } + + public ParisInvalideException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/exception/ParisTrioInvalideException.java b/src/main/java/com/pmumali/trio/exception/ParisTrioInvalideException.java new file mode 100644 index 0000000..1e9eaa8 --- /dev/null +++ b/src/main/java/com/pmumali/trio/exception/ParisTrioInvalideException.java @@ -0,0 +1,7 @@ +package com.pmumali.trio.exception; + +public class ParisTrioInvalideException extends RuntimeException { + public ParisTrioInvalideException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/model/Cagnotte.java b/src/main/java/com/pmumali/trio/model/Cagnotte.java new file mode 100644 index 0000000..6186a29 --- /dev/null +++ b/src/main/java/com/pmumali/trio/model/Cagnotte.java @@ -0,0 +1,57 @@ +package com.pmumali.trio.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Data +@Table(name = "cagnottes") +public class Cagnotte { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private double montant; + + @Column(nullable = false) + private LocalDateTime dateCreation; + + private LocalDateTime dateUtilisation; + + @Column(nullable = false) + private boolean utilisee = false; + + @ManyToOne + @JoinColumn(name = "course_source_id") + private Course courseSource; + + @ManyToOne + @JoinColumn(name = "course_destination_id") + private Course courseDestination; + + @Column(length = 500) + private String description; + + // Méthodes utilitaires + public void marquerCommeUtilisee(Course courseDestination) { + this.utilisee = true; + this.courseDestination = courseDestination; + this.dateUtilisation = LocalDateTime.now(); + this.description = "Cagnotte utilisée pour la course " + courseDestination.getNom(); + } + + public boolean estDisponible() { + return !utilisee && dateUtilisation == null; + } + + @PrePersist + protected void onCreate() { + if (dateCreation == null) { + dateCreation = LocalDateTime.now(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/model/Cheval.java b/src/main/java/com/pmumali/trio/model/Cheval.java new file mode 100644 index 0000000..a88b1b7 --- /dev/null +++ b/src/main/java/com/pmumali/trio/model/Cheval.java @@ -0,0 +1,17 @@ +package com.pmumali.trio.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; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/model/Course.java b/src/main/java/com/pmumali/trio/model/Course.java new file mode 100644 index 0000000..30685e4 --- /dev/null +++ b/src/main/java/com/pmumali/trio/model/Course.java @@ -0,0 +1,22 @@ +package com.pmumali.trio.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 chevaux; + private boolean estTerminee; + private boolean aDeadHeat; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/model/ParisTrio.java b/src/main/java/com/pmumali/trio/model/ParisTrio.java new file mode 100644 index 0000000..9e8a0e2 --- /dev/null +++ b/src/main/java/com/pmumali/trio/model/ParisTrio.java @@ -0,0 +1,43 @@ +package com.pmumali.trio.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Data +public class ParisTrio { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Course course; + + @ManyToOne + private Cheval cheval1; + + @ManyToOne + private Cheval cheval2; + + @ManyToOne + private Cheval cheval3; + + private double mise; + private LocalDateTime dateParis; + private String idParieur; + private StatutParis statut; + private Double gains; + private TypeFormule typeFormule; + + public enum StatutParis { + EN_ATTENTE, GAGNANT, PERDANT, REMBOURSE, + SPECIAL_JUMELE, SPECIAL_GAGNANT + } + + public enum TypeFormule { + UNITAIRE, COMBINEE_COMPLETE, COMBINEE_SIMPLIFIEE, + CHAMP_TOTAL, CHAMP_PARTIEL + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/model/ResultatCourse.java b/src/main/java/com/pmumali/trio/model/ResultatCourse.java new file mode 100644 index 0000000..f0a9f4f --- /dev/null +++ b/src/main/java/com/pmumali/trio/model/ResultatCourse.java @@ -0,0 +1,41 @@ +package com.pmumali.trio.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; + + @ManyToOne + private Cheval troisieme; + + @ElementCollection + private List chevauxDeadHeatPremierePlace; + + @ElementCollection + private List chevauxDeadHeatDeuxiemePlace; + + @ElementCollection + private List chevauxDeadHeatTroisiemePlace; + + private double totalMises; + private double masseAPartager; + private double prelevementsLegaux; + private double montantRembourse; + private double montantCagnotte; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/repository/CagnotteRepository.java b/src/main/java/com/pmumali/trio/repository/CagnotteRepository.java new file mode 100644 index 0000000..c725f90 --- /dev/null +++ b/src/main/java/com/pmumali/trio/repository/CagnotteRepository.java @@ -0,0 +1,29 @@ +package com.pmumali.trio.repository; + +import com.pmumali.trio.model.Cagnotte; +import com.pmumali.trio.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface CagnotteRepository extends JpaRepository { + + List findByUtiliseeFalse(); + + @Query("SELECT c FROM Cagnotte c WHERE c.utilisee = false AND " + + "c.dateCreation >= :dateDebut AND c.dateCreation <= :dateFin") + List findCagnottesDisponiblesEntreDates(LocalDateTime dateDebut, LocalDateTime dateFin); + + @Query("SELECT SUM(c.montant) FROM Cagnotte c WHERE c.utilisee = false") + Double getMontantTotalCagnotteDisponible(); + + @Query("SELECT c FROM Cagnotte c WHERE c.courseSource = :course") + Cagnotte findByCourseSource(Course course); + + @Query("UPDATE Cagnotte c SET c.utilisee = true WHERE c.id = :id") + void marquerCommeUtilisee(Long id); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/repository/ChevalRepository.java b/src/main/java/com/pmumali/trio/repository/ChevalRepository.java new file mode 100644 index 0000000..2617b1f --- /dev/null +++ b/src/main/java/com/pmumali/trio/repository/ChevalRepository.java @@ -0,0 +1,27 @@ +package com.pmumali.trio.repository; + +import com.pmumali.trio.model.Cheval; +import com.pmumali.trio.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ChevalRepository extends JpaRepository { + + List findByCourse(Course course); + + List findByCourseAndNonPartantFalse(Course course); + + List findByNonPartantTrue(); + + @Query("SELECT c FROM Cheval c WHERE c.course = :course AND " + + "(c.nom LIKE %:nom% OR c.id = :id)") + List findByCourseAndNomOrId(Course course, String nom, Long id); + + long countByCourse(Course course); + + boolean existsByNomAndCourse(String nom, Course course); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/repository/CourseRepository.java b/src/main/java/com/pmumali/trio/repository/CourseRepository.java new file mode 100644 index 0000000..962fb8a --- /dev/null +++ b/src/main/java/com/pmumali/trio/repository/CourseRepository.java @@ -0,0 +1,24 @@ +package com.pmumali.trio.repository; + +import com.pmumali.trio.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface CourseRepository extends JpaRepository { + + List findByEstTermineeFalse(); + + List findByHeureDebutAfter(LocalDateTime date); + + List findByHeureDebutBetween(LocalDateTime start, LocalDateTime end); + + @Query("SELECT c FROM Course c WHERE c.isTerminee = false AND SIZE(c.chevaux) BETWEEN 3 AND 7") + List findCoursesEligiblesPourTrio(); + + boolean existsByNomAndHeureDebut(String nom, LocalDateTime heureDebut); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/repository/ParisTrioRepository.java b/src/main/java/com/pmumali/trio/repository/ParisTrioRepository.java new file mode 100644 index 0000000..d277238 --- /dev/null +++ b/src/main/java/com/pmumali/trio/repository/ParisTrioRepository.java @@ -0,0 +1,44 @@ +package com.pmumali.trio.repository; + +import com.pmumali.trio.model.Course; +import com.pmumali.trio.model.Cheval; +import com.pmumali.trio.model.ParisTrio; +import com.pmumali.trio.model.ParisTrio.StatutParis; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ParisTrioRepository extends JpaRepository { + + List findByCourse(Course course); + + List findByCourseAndStatut(Course course, StatutParis statut); + + List findByIdParieur(String idParieur); + + @Query("SELECT p FROM ParisTrio p WHERE p.course = :course AND " + + "((p.cheval1 = :cheval1 AND p.cheval2 = :cheval2 AND p.cheval3 = :cheval3) OR " + + "(p.cheval1 = :cheval1 AND p.cheval2 = :cheval3 AND p.cheval3 = :cheval2) OR " + + "(p.cheval1 = :cheval2 AND p.cheval2 = :cheval1 AND p.cheval3 = :cheval3) OR " + + "(p.cheval1 = :cheval2 AND p.cheval2 = :cheval3 AND p.cheval3 = :cheval1) OR " + + "(p.cheval1 = :cheval3 AND p.cheval2 = :cheval1 AND p.cheval3 = :cheval2) OR " + + "(p.cheval1 = :cheval3 AND p.cheval2 = :cheval2 AND p.cheval3 = :cheval1))") + List findByCourseAndChevalCombinaison(Course course, Cheval cheval1, Cheval cheval2, Cheval cheval3); + + @Query("SELECT p FROM ParisTrio p WHERE p.course = :course AND " + + "p.statut = :statut AND (" + + "(p.cheval1.nonPartant = true AND p.cheval2.nonPartant = true) OR " + + "(p.cheval1.nonPartant = true AND p.cheval3.nonPartant = true) OR " + + "(p.cheval2.nonPartant = true AND p.cheval3.nonPartant = true))") + List findByCourseAndStatutAndTwoNonPartants(Course course, StatutParis statut); + + @Query("SELECT p FROM ParisTrio p WHERE p.course = :course AND " + + "p.statut = :statut AND " + + "(p.cheval1.nonPartant = true OR p.cheval2.nonPartant = true OR p.cheval3.nonPartant = true)") + List findByCourseAndStatutAndAnyNonPartant(Course course, StatutParis statut); + + long countByCourseAndStatut(Course course, StatutParis statut); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/trio/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..736c95e --- /dev/null +++ b/src/main/java/com/pmumali/trio/repository/ResultatCourseRepository.java @@ -0,0 +1,26 @@ +package com.pmumali.trio.repository; + +import com.pmumali.trio.model.Course; +import com.pmumali.trio.model.ResultatCourse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ResultatCourseRepository extends JpaRepository { + + ResultatCourse findByCourse(Course course); + + @Query("SELECT r FROM ResultatCourse r WHERE r.course = :course AND " + + "(SIZE(r.chevauxDeadHeatPremierePlace) > 0 OR " + + "SIZE(r.chevauxDeadHeatDeuxiemePlace) > 0 OR " + + "SIZE(r.chevauxDeadHeatTroisiemePlace) > 0)") + List findCoursesAvecDeadHeat(Course course); + + @Query("SELECT SUM(r.montantCagnotte) FROM ResultatCourse r WHERE r.montantCagnotte > 0") + Double getTotalCagnotteDisponible(); + + boolean existsByCourse(Course course); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/service/ParisTrioService.java b/src/main/java/com/pmumali/trio/service/ParisTrioService.java new file mode 100644 index 0000000..c10e6d4 --- /dev/null +++ b/src/main/java/com/pmumali/trio/service/ParisTrioService.java @@ -0,0 +1,87 @@ +package com.pmumali.trio.service; + +import com.pmumali.jumeleordre.exception.ChevalInvalideException; +import com.pmumali.trio.dto.ParisTrioDto; +import com.pmumali.trio.exception.*; +import com.pmumali.trio.model.*; +import com.pmumali.trio.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ParisTrioService { + private static final double MISE_MINIMUM = 500; + private static final double MISE_MAXIMUM_PAR_PARIS = 20 * MISE_MINIMUM; + + @Autowired + private ParisTrioRepository parisTrioRepository; + @Autowired + private CourseRepository courseRepository; + @Autowired + private ChevalRepository chevalRepository; + + @Transactional + public ParisTrio enregistrerParis(ParisTrioDto parisDto) throws ParisInvalideException { + // Validation de la course + Course course = courseRepository.findById(parisDto.getIdCourse()) + .orElseThrow(() -> new CourseInvalideException("Course non trouvée")); + + if (course.isEstTerminee()) { + throw new CourseInvalideException("Impossible de parier sur une course terminée"); + } + + // Validation des chevaux + Cheval cheval1 = chevalRepository.findById(parisDto.getIdCheval1()) + .orElseThrow(() -> new ChevalInvalideException("Cheval 1 non trouvé")); + Cheval cheval2 = chevalRepository.findById(parisDto.getIdCheval2()) + .orElseThrow(() -> new ChevalInvalideException("Cheval 2 non trouvé")); + Cheval cheval3 = chevalRepository.findById(parisDto.getIdCheval3()) + .orElseThrow(() -> new ChevalInvalideException("Cheval 3 non trouvé")); + + // Vérification des doublons + if (cheval1.equals(cheval2) || cheval1.equals(cheval3) || cheval2.equals(cheval3)) { + throw new ParisInvalideException("Les trois chevaux doivent être différents"); + } + + // Vérification des non-partants + if (cheval1.isNonPartant() || cheval2.isNonPartant() || cheval3.isNonPartant()) { + throw new ParisInvalideException("Impossible de parier sur des chevaux non partants"); + } + + // Vérification de la mise + if (parisDto.getMise() < MISE_MINIMUM) { + throw new ParisInvalideException("La mise doit être d'au moins " + MISE_MINIMUM + " FCFA"); + } + + // Vérification de la limite de mise + double totalMisesCombinaison = parisTrioRepository + .findByCourseAndChevalCombinaison( + course, cheval1, cheval2, cheval3) + .stream() + .mapToDouble(ParisTrio::getMise) + .sum(); + + if (totalMisesCombinaison + parisDto.getMise() > MISE_MAXIMUM_PAR_PARIS) { + throw new LimiteMiseDepasseeException(totalMisesCombinaison + parisDto.getMise(), + MISE_MAXIMUM_PAR_PARIS); + } + + // Création du pari + ParisTrio paris = new ParisTrio(); + paris.setCourse(course); + paris.setCheval1(cheval1); + paris.setCheval2(cheval2); + paris.setCheval3(cheval3); + paris.setMise(parisDto.getMise()); + paris.setDateParis(LocalDateTime.now()); + paris.setIdParieur(parisDto.getIdParieur()); + paris.setStatut(ParisTrio.StatutParis.EN_ATTENTE); + paris.setTypeFormule(parisDto.getTypeFormule()); + + return parisTrioRepository.save(paris); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trio/service/ResultatCourseService.java b/src/main/java/com/pmumali/trio/service/ResultatCourseService.java new file mode 100644 index 0000000..8780434 --- /dev/null +++ b/src/main/java/com/pmumali/trio/service/ResultatCourseService.java @@ -0,0 +1,189 @@ +package com.pmumali.trio.service; + +import com.pmumali.jumeleordre.exception.ResultatCourseInvalideException; +import com.pmumali.trio.dto.ResultatCourseDto; +import com.pmumali.trio.exception.*; +import com.pmumali.trio.model.*; +import com.pmumali.trio.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ResultatCourseService { + @Autowired private CourseRepository courseRepository; + @Autowired private ChevalRepository chevalRepository; + @Autowired private ParisTrioRepository parisTrioRepository; + @Autowired private ResultatCourseRepository resultatCourseRepository; + @Autowired private CagnotteRepository cagnotteRepository; + + @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"); + } + + // Validation des chevaux + Cheval premier = validerCheval(resultatDto.getIdChevalPremier(), "premier"); + Cheval deuxieme = validerCheval(resultatDto.getIdChevalDeuxieme(), "deuxième"); + Cheval troisieme = validerCheval(resultatDto.getIdChevalTroisieme(), "troisième"); + + // Enregistrement du résultat + ResultatCourse resultat = new ResultatCourse(); + resultat.setCourse(course); + resultat.setPremier(premier); + resultat.setDeuxieme(deuxieme); + resultat.setTroisieme(troisieme); + resultat.setChevauxDeadHeatPremierePlace(resultatDto.getChevauxDeadHeatPremierePlace()); + resultat.setChevauxDeadHeatDeuxiemePlace(resultatDto.getChevauxDeadHeatDeuxiemePlace()); + resultat.setChevauxDeadHeatTroisiemePlace(resultatDto.getChevauxDeadHeatTroisiemePlace()); + + // Calcul des montants + List tousParis = parisTrioRepository.findByCourse(course); + double totalMises = tousParis.stream().mapToDouble(ParisTrio::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); + + // Traitement des paris + traiterParis(tousParis, resultat); + + // Gestion de la cagnotte si nécessaire + if (resultat.getMontantCagnotte() > 0) { + Cagnotte cagnotte = new Cagnotte(); + cagnotte.setMontant(resultat.getMontantCagnotte()); + cagnotte.setDateCreation(LocalDateTime.now()); + cagnotte.setCourseSource(course); + cagnotteRepository.save(cagnotte); + } + + // Marquer la course comme terminée + course.setEstTerminee(true); + course.setADeadHeat(resultatDto.hasDeadHeat()); + courseRepository.save(course); + resultatCourseRepository.save(resultat); + } + + private Cheval validerCheval(Long idCheval, String position) throws ResultatCourseInvalideException { + if (idCheval == null) { + throw new ResultatCourseInvalideException("Cheval " + position + " non spécifié"); + } + return chevalRepository.findById(idCheval) + .orElseThrow(() -> new ResultatCourseInvalideException("Cheval " + position + " non trouvé")); + } + + private double calculerPrelevementsLegaux(double totalMises) { + // 15% de prélèvements légaux + return totalMises * 0.15; + } + + private double calculerMontantRembourse(List paris) { + return paris.stream() + .filter(p -> p.getCheval1().isNonPartant() && + p.getCheval2().isNonPartant() && + p.getCheval3().isNonPartant()) + .mapToDouble(ParisTrio::getMise) + .sum(); + } + + private void traiterParis(List paris, ResultatCourse resultat) { + boolean deadHeatPremiere = !resultat.getChevauxDeadHeatPremierePlace().isEmpty(); + boolean deadHeatDeuxieme = !resultat.getChevauxDeadHeatDeuxiemePlace().isEmpty(); + boolean deadHeatTroisieme = !resultat.getChevauxDeadHeatTroisiemePlace().isEmpty(); + + for (ParisTrio p : paris) { + // Cas des non-partants (Article 4) + if (p.getCheval1().isNonPartant() || p.getCheval2().isNonPartant() || p.getCheval3().isNonPartant()) { + traiterParisAvecNonPartants(p, resultat); + continue; + } + + // Cas normal ou dead heat + if (estParisGagnant(p, resultat, deadHeatPremiere, deadHeatDeuxieme, deadHeatTroisieme)) { + p.setStatut(ParisTrio.StatutParis.GAGNANT); + p.setGains(calculerGains(p, resultat)); + } else { + p.setStatut(ParisTrio.StatutParis.PERDANT); + p.setGains(0.0); + } + } + } + + private void traiterParisAvecNonPartants(ParisTrio paris, ResultatCourse resultat) { + int nbNonPartants = compterNonPartants(paris); + + if (nbNonPartants == 3) { + paris.setStatut(ParisTrio.StatutParis.REMBOURSE); + paris.setGains(paris.getMise()); + } else if (nbNonPartants == 2) { + if (estSpecialGagnant(paris, resultat)) { + paris.setStatut(ParisTrio.StatutParis.SPECIAL_GAGNANT); + paris.setGains(paris.getMise() * 30.0); // 30 fois la mise (1/4 du rapport TRIO) + } else { + paris.setStatut(ParisTrio.StatutParis.PERDANT); + paris.setGains(0.0); + } + } else if (nbNonPartants == 1) { + if (estSpecialJumele(paris, resultat)) { + paris.setStatut(ParisTrio.StatutParis.SPECIAL_JUMELE); + paris.setGains(paris.getMise() * 60.0); // 60 fois la mise (1/2 du rapport TRIO) + } else { + paris.setStatut(ParisTrio.StatutParis.PERDANT); + paris.setGains(0.0); + } + } + } + + private boolean estSpecialGagnant(ParisTrio paris, ResultatCourse resultat) { + Cheval chevalPartant = paris.getCheval1().isNonPartant() ? + (paris.getCheval2().isNonPartant() ? paris.getCheval3() : paris.getCheval2()) : + paris.getCheval1(); + + return chevalPartant.equals(resultat.getPremier()); + } + + private boolean estSpecialJumele(ParisTrio paris, ResultatCourse resultat) { + Cheval nonPartant = paris.getCheval1().isNonPartant() ? paris.getCheval1() : + (paris.getCheval2().isNonPartant() ? paris.getCheval2() : paris.getCheval3()); + + List chevauxPartants = List.of(paris.getCheval1(), paris.getCheval2(), paris.getCheval3()) + .stream() + .filter(c -> !c.equals(nonPartant)) + .collect(Collectors.toList()); + + return (chevauxPartants.contains(resultat.getPremier()) && + chevauxPartants.contains(resultat.getDeuxieme())); + } + + private boolean estParisGagnant(ParisTrio paris, ResultatCourse resultat, + boolean deadHeatPremiere, boolean deadHeatDeuxieme, boolean deadHeatTroisieme) { + // Implémentation complexe des règles de dead heat selon l'article 3 + // ... (logique similaire à celle vue précédemment mais adaptée pour 3 chevaux) + return false; + } + + private double calculerGains(ParisTrio paris, ResultatCourse resultat) { + // Calcul des gains selon les règles spécifiques + return Math.max(paris.getMise() * 1.1, paris.getMise() * 5.0); // Minimum 1.1 + } + + private int compterNonPartants(ParisTrio paris) { + int count = 0; + if (paris.getCheval1().isNonPartant()) count++; + if (paris.getCheval2().isNonPartant()) count++; + if (paris.getCheval3().isNonPartant()) count++; + return count; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/controller/ParisTrioOrdreController.java b/src/main/java/com/pmumali/trioordre/controller/ParisTrioOrdreController.java new file mode 100644 index 0000000..0e76f3b --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/controller/ParisTrioOrdreController.java @@ -0,0 +1,25 @@ +package com.pmumali.trioordre.controller; + +import com.pmumali.trioordre.dto.ParisTrioOrdreDto; +import com.pmumali.trioordre.model.ParisTrioOrdre; +import com.pmumali.trioordre.service.ParisTrioOrdreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/paris-trio-ordre") +public class ParisTrioOrdreController { + @Autowired + private ParisTrioOrdreService parisService; + + @PostMapping + public ResponseEntity enregistrerParis(@RequestBody ParisTrioOrdreDto parisDto) { + try { + ParisTrioOrdre paris = parisService.enregistrerParis(parisDto); + return ResponseEntity.ok(paris); + } catch (Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/controller/ResultatCourseController.java b/src/main/java/com/pmumali/trioordre/controller/ResultatCourseController.java new file mode 100644 index 0000000..4e48ed0 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/controller/ResultatCourseController.java @@ -0,0 +1,24 @@ +package com.pmumali.trioordre.controller; + +import com.pmumali.trioordre.dto.ResultatCourseDto; +import com.pmumali.trioordre.service.ResultatCourseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/resultats") +public class ResultatCourseController { + @Autowired + private ResultatCourseService resultatService; + + @PostMapping + public ResponseEntity enregistrerResultat(@RequestBody ResultatCourseDto resultatDto) { + try { + resultatService.traiterResultatCourse(resultatDto); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/dto/ParisTrioOrdreDto.java b/src/main/java/com/pmumali/trioordre/dto/ParisTrioOrdreDto.java new file mode 100644 index 0000000..2006ceb --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/dto/ParisTrioOrdreDto.java @@ -0,0 +1,15 @@ +package com.pmumali.trioordre.dto; + +import com.pmumali.trioordre.model.ParisTrioOrdre.TypeFormule; +import lombok.Data; + +@Data +public class ParisTrioOrdreDto { + private Long idCourse; + private Long idPremier; + private Long idDeuxieme; + private Long idTroisieme; + private double mise; + private String idParieur; + private TypeFormule typeFormule; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/dto/ResultatCourseDto.java b/src/main/java/com/pmumali/trioordre/dto/ResultatCourseDto.java new file mode 100644 index 0000000..8d75b5b --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/dto/ResultatCourseDto.java @@ -0,0 +1,16 @@ +package com.pmumali.trioordre.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ResultatCourseDto { + private Long idCourse; + private Long idPremier; + private Long idDeuxieme; + private Long idTroisieme; + private List chevauxDeadHeatPremierePlace; + private List chevauxDeadHeatDeuxiemePlace; + private List chevauxDeadHeatTroisiemePlace; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/CagnotteDejaUtiliseeException.java b/src/main/java/com/pmumali/trioordre/exception/CagnotteDejaUtiliseeException.java new file mode 100644 index 0000000..3a0dcdd --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CagnotteDejaUtiliseeException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CagnotteDejaUtiliseeException extends CagnotteException { + public CagnotteDejaUtiliseeException(Long idCagnotte) { + super(String.format("La cagnotte %d a déjà été utilisée", idCagnotte)); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/CagnotteException.java b/src/main/java/com/pmumali/trioordre/exception/CagnotteException.java new file mode 100644 index 0000000..68fcef0 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CagnotteException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CagnotteException extends TrioOrdreException { + public CagnotteException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/CagnotteNonDisponibleException.java b/src/main/java/com/pmumali/trioordre/exception/CagnotteNonDisponibleException.java new file mode 100644 index 0000000..05d5904 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CagnotteNonDisponibleException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CagnotteNonDisponibleException extends CagnotteException { + public CagnotteNonDisponibleException() { + super("Aucune cagnotte disponible"); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/ChevalException.java b/src/main/java/com/pmumali/trioordre/exception/ChevalException.java new file mode 100644 index 0000000..d9a7f4b --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ChevalException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ChevalException extends TrioOrdreException { + public ChevalException(String message) { + super(message); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/ChevalNonPartantException.java b/src/main/java/com/pmumali/trioordre/exception/ChevalNonPartantException.java new file mode 100644 index 0000000..47c0456 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ChevalNonPartantException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ChevalNonPartantException extends ChevalException { + public ChevalNonPartantException(String nomCheval) { + super(String.format("Le cheval %s est non-partant", nomCheval)); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/ChevalNonTrouveException.java b/src/main/java/com/pmumali/trioordre/exception/ChevalNonTrouveException.java new file mode 100644 index 0000000..b62caff --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ChevalNonTrouveException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ChevalNonTrouveException extends ChevalException { + public ChevalNonTrouveException(Long idCheval) { + super(String.format("Cheval non trouvé avec l'ID: %d", idCheval)); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/CombinaisonInvalideException.java b/src/main/java/com/pmumali/trioordre/exception/CombinaisonInvalideException.java new file mode 100644 index 0000000..d45fbdb --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CombinaisonInvalideException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CombinaisonInvalideException extends ParisTrioOrdreException { + public CombinaisonInvalideException() { + super("Combinaison invalide: les 3 chevaux doivent être différents"); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/CourseDejaTermineeException.java b/src/main/java/com/pmumali/trioordre/exception/CourseDejaTermineeException.java new file mode 100644 index 0000000..1260fe9 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CourseDejaTermineeException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CourseDejaTermineeException extends CourseException { + public CourseDejaTermineeException(Long idCourse) { + super(String.format("La course %d est déjà terminée", idCourse)); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/CourseException.java b/src/main/java/com/pmumali/trioordre/exception/CourseException.java new file mode 100644 index 0000000..4e63ab3 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CourseException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class CourseException extends TrioOrdreException { + public CourseException(String message) { + super(message); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/CourseNonEligibleException.java b/src/main/java/com/pmumali/trioordre/exception/CourseNonEligibleException.java new file mode 100644 index 0000000..8050fde --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/CourseNonEligibleException.java @@ -0,0 +1,10 @@ +package com.pmumali.trioordre.exception; + +public class CourseNonEligibleException extends CourseException { + private final int nombreChevaux; + + public CourseNonEligibleException(int nombreChevaux) { + super(String.format("Course non éligible pour TRIO-ORDRE: %d chevaux (doit être entre 3 et 7)", nombreChevaux)); + this.nombreChevaux = nombreChevaux; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/GlobalExceptionHandler.java b/src/main/java/com/pmumali/trioordre/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..8533cdc --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/GlobalExceptionHandler.java @@ -0,0 +1,65 @@ +package com.pmumali.trioordre.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 java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private static final String TIMESTAMP = "timestamp"; + private static final String MESSAGE = "message"; + private static final String TYPE = "type"; + private static final String STATUS = "status"; + + @ExceptionHandler(TrioOrdreException.class) + public ResponseEntity handleTrioOrdreException( + TrioOrdreException ex, WebRequest request) { + + Map 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("miseTotale", specificEx.getMiseTotale()); + body.put("limite", specificEx.getLimite()); + } else if (ex instanceof MiseInvalideException) { + MiseInvalideException specificEx = (MiseInvalideException) ex; + body.put("mise", specificEx.getMise()); + body.put("miseMinimum", specificEx.getMiseMinimum()); + } else if (ex instanceof ResultatDejaEnregistreException) { + status = HttpStatus.CONFLICT; + } else if (ex instanceof CagnotteNonDisponibleException) { + status = HttpStatus.NOT_FOUND; + } + + body.put(STATUS, status.value()); + + return new ResponseEntity<>(body, status); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException( + Exception ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put(TIMESTAMP, LocalDateTime.now()); + body.put(MESSAGE, "Une erreur interne est survenue"); + body.put(TYPE, "ErreurInterne"); + body.put(STATUS, HttpStatus.INTERNAL_SERVER_ERROR.value()); + + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/LimiteMiseDepasseeException.java b/src/main/java/com/pmumali/trioordre/exception/LimiteMiseDepasseeException.java new file mode 100644 index 0000000..52288bc --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/LimiteMiseDepasseeException.java @@ -0,0 +1,16 @@ +package com.pmumali.trioordre.exception; + +import lombok.Data; +import lombok.Getter; + +@Getter +public class LimiteMiseDepasseeException extends ParisTrioOrdreException { + private final double miseTotale; + private final double limite; + + public LimiteMiseDepasseeException(double miseTotale, double limite) { + super(String.format("Limite de mise dépassée: %.2f FCFA (limite: %.2f FCFA)", miseTotale, limite)); + this.miseTotale = miseTotale; + this.limite = limite; + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/MiseInvalideException.java b/src/main/java/com/pmumali/trioordre/exception/MiseInvalideException.java new file mode 100644 index 0000000..4f2ef0d --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/MiseInvalideException.java @@ -0,0 +1,16 @@ +package com.pmumali.trioordre.exception; + +import lombok.Getter; + +@Getter + +public class MiseInvalideException extends ParisTrioOrdreException { + private final double mise; + private final double miseMinimum; + + public MiseInvalideException(double mise, double miseMinimum) { + super(String.format("Mise invalide: %.2f FCFA (minimum: %.2f FCFA)", mise, miseMinimum)); + this.mise = mise; + this.miseMinimum = miseMinimum; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/ParisTrioOrdreException.java b/src/main/java/com/pmumali/trioordre/exception/ParisTrioOrdreException.java new file mode 100644 index 0000000..27a07fa --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ParisTrioOrdreException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ParisTrioOrdreException extends TrioOrdreException { + public ParisTrioOrdreException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/ResultatCourseException.java b/src/main/java/com/pmumali/trioordre/exception/ResultatCourseException.java new file mode 100644 index 0000000..4267e5c --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ResultatCourseException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ResultatCourseException extends TrioOrdreException { + public ResultatCourseException(String message) { + super(message); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/ResultatCourseInvalideException.java b/src/main/java/com/pmumali/trioordre/exception/ResultatCourseInvalideException.java new file mode 100644 index 0000000..dd4a514 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ResultatCourseInvalideException.java @@ -0,0 +1,14 @@ +// ResultatCourseInvalideException.java +package com.pmumali.trioordre.exception; + +import com.pmumali.jumeleordre.exception.JumeleOrdreException; + +public class ResultatCourseInvalideException extends JumeleOrdreException { + public ResultatCourseInvalideException(String message) { + super(message); + } + + public ResultatCourseInvalideException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/exception/ResultatDejaEnregistreException.java b/src/main/java/com/pmumali/trioordre/exception/ResultatDejaEnregistreException.java new file mode 100644 index 0000000..8b329c9 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ResultatDejaEnregistreException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ResultatDejaEnregistreException extends ResultatCourseException { + public ResultatDejaEnregistreException(Long idCourse) { + super(String.format("Un résultat existe déjà pour la course %d", idCourse)); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/ResultatIncompletException.java b/src/main/java/com/pmumali/trioordre/exception/ResultatIncompletException.java new file mode 100644 index 0000000..5915098 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/ResultatIncompletException.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.exception; + +public class ResultatIncompletException extends ResultatCourseException { + public ResultatIncompletException() { + super("Résultat incomplet: les 3 premières positions doivent être spécifiées"); + } +} diff --git a/src/main/java/com/pmumali/trioordre/exception/TrioOrdreException.java b/src/main/java/com/pmumali/trioordre/exception/TrioOrdreException.java new file mode 100644 index 0000000..4e46de8 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/exception/TrioOrdreException.java @@ -0,0 +1,11 @@ +package com.pmumali.trioordre.exception; + +public class TrioOrdreException extends RuntimeException { + public TrioOrdreException(String message) { + super(message); + } + + public TrioOrdreException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/model/Cagnotte.java b/src/main/java/com/pmumali/trioordre/model/Cagnotte.java new file mode 100644 index 0000000..dae08c9 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/model/Cagnotte.java @@ -0,0 +1,31 @@ +package com.pmumali.trioordre.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Data +public class Cagnotte { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private double montant; + private LocalDateTime dateCreation; + private LocalDateTime dateUtilisation; + private boolean utilisee = false; + + @ManyToOne + private Course courseSource; + + @ManyToOne + private Course courseDestination; + + public void utiliserPourCourse(Course course) { + this.utilisee = true; + this.courseDestination = course; + this.dateUtilisation = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/model/Cheval.java b/src/main/java/com/pmumali/trioordre/model/Cheval.java new file mode 100644 index 0000000..76c8ea1 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/model/Cheval.java @@ -0,0 +1,17 @@ +package com.pmumali.trioordre.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; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/model/Course.java b/src/main/java/com/pmumali/trioordre/model/Course.java new file mode 100644 index 0000000..32481cd --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/model/Course.java @@ -0,0 +1,26 @@ +package com.pmumali.trioordre.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 chevaux; + private boolean estTerminee; + private boolean aDeadHeat; + + public boolean estEligiblePourTrioOrdre() { + return chevaux != null && chevaux.size() >= 3 && chevaux.size() <= 7; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/model/ParisTrioOrdre.java b/src/main/java/com/pmumali/trioordre/model/ParisTrioOrdre.java new file mode 100644 index 0000000..48d4dc3 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/model/ParisTrioOrdre.java @@ -0,0 +1,51 @@ +package com.pmumali.trioordre.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Data +public class ParisTrioOrdre { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Course course; + + @ManyToOne + private Cheval premier; + + @ManyToOne + private Cheval deuxieme; + + @ManyToOne + private Cheval troisieme; + + private double mise; + private LocalDateTime dateParis; + private String idParieur; + private StatutParis statut; + private Double gains; + private TypeFormule typeFormule; + + public enum StatutParis { + EN_ATTENTE, GAGNANT, PERDANT, REMBOURSE, + SPECIAL_JUMELE, SPECIAL_GAGNANT + } + + public enum TypeFormule { + UNITAIRE, + COMBINEE_COMPLETE, COMBINEE_SIMPLIFIEE, + CHAMP_TOTAL_2_CHEVAUX_COMPLET, CHAMP_TOTAL_2_CHEVAUX_SIMPLIFIE, + CHAMP_PARTIEL_2_CHEVAUX_COMPLET, CHAMP_PARTIEL_2_CHEVAUX_SIMPLIFIE, + CHAMP_TOTAL_1_CHEVAL_COMPLET, CHAMP_TOTAL_1_CHEVAL_SIMPLIFIE, + CHAMP_PARTIEL_1_CHEVAL_COMPLET, CHAMP_PARTIEL_1_CHEVAL_SIMPLIFIE + } + + public boolean estFormuleChamp() { + return typeFormule.name().contains("CHAMP"); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/model/ResultatCourse.java b/src/main/java/com/pmumali/trioordre/model/ResultatCourse.java new file mode 100644 index 0000000..e96ae46 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/model/ResultatCourse.java @@ -0,0 +1,47 @@ +package com.pmumali.trioordre.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; + + @ManyToOne + private Cheval troisieme; + + @ElementCollection + private List chevauxDeadHeatPremierePlace; + + @ElementCollection + private List chevauxDeadHeatDeuxiemePlace; + + @ElementCollection + private List chevauxDeadHeatTroisiemePlace; + + private double totalMises; + private double masseAPartager; + private double prelevementsLegaux; + private double montantRembourse; + private double montantCagnotte; + + public boolean hasDeadHeat() { + return !chevauxDeadHeatPremierePlace.isEmpty() || + !chevauxDeadHeatDeuxiemePlace.isEmpty() || + !chevauxDeadHeatTroisiemePlace.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/repository/CagnotteRepository.java b/src/main/java/com/pmumali/trioordre/repository/CagnotteRepository.java new file mode 100644 index 0000000..94f1333 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/repository/CagnotteRepository.java @@ -0,0 +1,14 @@ +package com.pmumali.trioordre.repository; + +import com.pmumali.trioordre.model.Cagnotte; +import com.pmumali.trioordre.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CagnotteRepository extends JpaRepository { + List findByUtiliseeFalse(); + List findByCourseSource(Course course); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/repository/ChevalRepository.java b/src/main/java/com/pmumali/trioordre/repository/ChevalRepository.java new file mode 100644 index 0000000..aa82c06 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/repository/ChevalRepository.java @@ -0,0 +1,54 @@ +package com.pmumali.trioordre.repository; + +import com.pmumali.trioordre.model.Cheval; +import com.pmumali.trioordre.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChevalRepository extends JpaRepository { + + // Trouver tous les chevaux d'une course + List findByCourse(Course course); + + // Trouver les chevaux partants d'une course + List findByCourseAndNonPartantFalse(Course course); + + // Trouver les chevaux non-partants d'une course + List findByCourseAndNonPartantTrue(Course course); + + // Trouver un cheval par son nom dans une course + Optional findByNomAndCourse(String nom, Course course); + + // Vérifier si un cheval est utilisé dans des paris + @Query("SELECT CASE WHEN COUNT(p) > 0 THEN true ELSE false END " + + "FROM ParisTrioOrdre p WHERE " + + "p.premier = :cheval OR p.deuxieme = :cheval OR p.troisieme = :cheval") + boolean isChevalUtiliseDansParis(Cheval cheval); + + // Trouver les chevaux gagnants (première place) + @Query("SELECT r.premier FROM ResultatCourse r WHERE r.course = :course") + Optional findChevalPremierByCourse(Course course); + + // Trouver les chevaux deuxièmes + @Query("SELECT r.deuxieme FROM ResultatCourse r WHERE r.course = :course") + Optional findChevalDeuxiemeByCourse(Course course); + + // Trouver les chevaux troisièmes + @Query("SELECT r.troisieme FROM ResultatCourse r WHERE r.course = :course") + Optional findChevalTroisiemeByCourse(Course course); + + // Trouver les chevaux en dead-heat pour une course + @Query("SELECT c FROM Cheval c WHERE c.course = :course AND " + + "(c.id IN (SELECT rd.chevauxDeadHeatPremierePlace FROM ResultatCourse rd WHERE rd.course = :course) OR " + + "c.id IN (SELECT rd.chevauxDeadHeatDeuxiemePlace FROM ResultatCourse rd WHERE rd.course = :course) OR " + + "c.id IN (SELECT rd.chevauxDeadHeatTroisiemePlace FROM ResultatCourse rd WHERE rd.course = :course))") + List findChevauxDeadHeatByCourse(Course course); + + // Compter les chevaux partants par course + long countByCourseAndNonPartantFalse(Course course); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/repository/CourseRepository.java b/src/main/java/com/pmumali/trioordre/repository/CourseRepository.java new file mode 100644 index 0000000..15efdaf --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/repository/CourseRepository.java @@ -0,0 +1,47 @@ +package com.pmumali.trioordre.repository; + +import com.pmumali.trioordre.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface CourseRepository extends JpaRepository { + + // Trouver les courses éligibles pour TRIO-ORDRE (3 à 7 chevaux non-partants) + @Query("SELECT c FROM Course c WHERE c.estTerminee = false AND " + + "SIZE(c.chevaux) BETWEEN 3 AND 7 AND " + + "(SELECT COUNT(ch) FROM Cheval ch WHERE ch.course = c AND ch.nonPartant = false) >= 3") + List findCoursesEligiblesPourTrioOrdre(); + + // Trouver les courses non terminées par date + List findByEstTermineeFalseAndHeureDebutAfter(LocalDateTime date); + + // Trouver une course par son nom et sa date + Optional findByNomAndHeureDebut(String nom, LocalDateTime heureDebut); + + // Vérifier si une course a des paris associés + @Query("SELECT CASE WHEN COUNT(p) > 0 THEN true ELSE false END " + + "FROM ParisTrioOrdre p WHERE p.course = :course") + boolean hasParisAssocies(Course course); + + // Compter les chevaux partants pour une course + @Query("SELECT COUNT(c) FROM Cheval c WHERE c.course = :course AND c.nonPartant = false") + long countChevauxPartantsByCourse(Course course); + + // Trouver les courses avec dead-heat + @Query("SELECT DISTINCT r.course FROM ResultatCourse r WHERE " + + "SIZE(r.chevauxDeadHeatPremierePlace) > 0 OR " + + "SIZE(r.chevauxDeadHeatDeuxiemePlace) > 0 OR " + + "SIZE(r.chevauxDeadHeatTroisiemePlace) > 0") + List findCoursesAvecDeadHeat(); + + // Trouver les courses terminées sans résultat enregistré + @Query("SELECT c FROM Course c WHERE c.estTerminee = true AND " + + "NOT EXISTS (SELECT r FROM ResultatCourse r WHERE r.course = c)") + List findCoursesTermineesSansResultat(); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/repository/ParisTrioOrdreRepository.java b/src/main/java/com/pmumali/trioordre/repository/ParisTrioOrdreRepository.java new file mode 100644 index 0000000..f908e06 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/repository/ParisTrioOrdreRepository.java @@ -0,0 +1,15 @@ +package com.pmumali.trioordre.repository; + +import com.pmumali.trioordre.model.*; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ParisTrioOrdreRepository extends JpaRepository { + List findByCourse(Course course); + List findByCourseAndPremierAndDeuxiemeAndTroisieme( + Course course, Cheval premier, Cheval deuxieme, Cheval troisieme); + List findByIdParieur(String idParieur); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/trioordre/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..9c00400 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/repository/ResultatCourseRepository.java @@ -0,0 +1,7 @@ +package com.pmumali.trioordre.repository; + +import com.pmumali.trioordre.model.ResultatCourse; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ResultatCourseRepository extends JpaRepository { +} diff --git a/src/main/java/com/pmumali/trioordre/service/ParisTrioOrdreService.java b/src/main/java/com/pmumali/trioordre/service/ParisTrioOrdreService.java new file mode 100644 index 0000000..9096675 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/service/ParisTrioOrdreService.java @@ -0,0 +1,81 @@ +package com.pmumali.trioordre.service; + +import com.pmumali.jumeleordre.exception.ChevalInvalideException; +import com.pmumali.trio.exception.CourseInvalideException; +import com.pmumali.trio.exception.ParisInvalideException; +import com.pmumali.trioordre.dto.ParisTrioOrdreDto; +import com.pmumali.trioordre.exception.*; +import com.pmumali.trioordre.model.*; +import com.pmumali.trioordre.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ParisTrioOrdreService { + private static final double MISE_MINIMUM = 500; + private static final double MISE_MAXIMUM_PAR_PARIS = 20 * MISE_MINIMUM; + + @Autowired + private ParisTrioOrdreRepository parisRepository; + @Autowired + private CourseRepository courseRepository; + @Autowired + private ChevalRepository chevalRepository; + + @Transactional + public ParisTrioOrdre enregistrerParis(ParisTrioOrdreDto parisDto) throws ParisInvalideException { + Course course = courseRepository.findById(parisDto.getIdCourse()) + .orElseThrow(() -> new CourseInvalideException("Course non trouvée")); + + if (!course.estEligiblePourTrioOrdre()) { + throw new CourseInvalideException("La course doit avoir entre 3 et 7 chevaux pour le TRIO-ORDRE"); + } + + Cheval premier = chevalRepository.findById(parisDto.getIdPremier()) + .orElseThrow(() -> new ChevalInvalideException("Cheval premier non trouvé")); + Cheval deuxieme = chevalRepository.findById(parisDto.getIdDeuxieme()) + .orElseThrow(() -> new ChevalInvalideException("Cheval deuxième non trouvé")); + Cheval troisieme = chevalRepository.findById(parisDto.getIdTroisieme()) + .orElseThrow(() -> new ChevalInvalideException("Cheval troisième non trouvé")); + + if (premier.equals(deuxieme) || premier.equals(troisieme) || deuxieme.equals(troisieme)) { + throw new ParisInvalideException("Les trois chevaux doivent être différents"); + } + + if (premier.isNonPartant() || deuxieme.isNonPartant() || troisieme.isNonPartant()) { + throw new ChevalNonPartantException("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"); + } + + double totalMisesCombinaison = parisRepository + .findByCourseAndPremierAndDeuxiemeAndTroisieme(course, premier, deuxieme, troisieme) + .stream() + .mapToDouble(ParisTrioOrdre::getMise) + .sum(); + + if (totalMisesCombinaison + parisDto.getMise() > MISE_MAXIMUM_PAR_PARIS) { + throw new LimiteMiseDepasseeException(totalMisesCombinaison + parisDto.getMise(), + MISE_MAXIMUM_PAR_PARIS); + } + + ParisTrioOrdre paris = new ParisTrioOrdre(); + paris.setCourse(course); + paris.setPremier(premier); + paris.setDeuxieme(deuxieme); + paris.setTroisieme(troisieme); + paris.setMise(parisDto.getMise()); + paris.setDateParis(LocalDateTime.now()); + paris.setIdParieur(parisDto.getIdParieur()); + paris.setStatut(ParisTrioOrdre.StatutParis.EN_ATTENTE); + paris.setTypeFormule(parisDto.getTypeFormule()); + + return parisRepository.save(paris); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/trioordre/service/ResultatCourseService.java b/src/main/java/com/pmumali/trioordre/service/ResultatCourseService.java new file mode 100644 index 0000000..47b9189 --- /dev/null +++ b/src/main/java/com/pmumali/trioordre/service/ResultatCourseService.java @@ -0,0 +1,223 @@ +package com.pmumali.trioordre.service; + + +import com.pmumali.trioordre.dto.ResultatCourseDto; +import com.pmumali.trioordre.exception.*; +import com.pmumali.trioordre.model.*; +import com.pmumali.trioordre.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class ResultatCourseService { + @Autowired private CourseRepository courseRepository; + @Autowired private ChevalRepository chevalRepository; + @Autowired private ParisTrioOrdreRepository parisRepository; + @Autowired private ResultatCourseRepository resultatRepository; + @Autowired private CagnotteRepository cagnotteRepository; + + @Transactional + public void traiterResultatCourse(ResultatCourseDto resultatDto) throws ResultatCourseInvalideException { + Course course = courseRepository.findById(resultatDto.getIdCourse()) + .orElseThrow(() -> new ResultatCourseInvalideException("Course non trouvée")); + + Cheval premier = validerCheval(resultatDto.getIdPremier(), "premier"); + Cheval deuxieme = validerCheval(resultatDto.getIdDeuxieme(), "deuxième"); + Cheval troisieme = validerCheval(resultatDto.getIdTroisieme(), "troisième"); + + ResultatCourse resultat = new ResultatCourse(); + resultat.setCourse(course); + resultat.setPremier(premier); + resultat.setDeuxieme(deuxieme); + resultat.setTroisieme(troisieme); + resultat.setChevauxDeadHeatPremierePlace(resultatDto.getChevauxDeadHeatPremierePlace()); + resultat.setChevauxDeadHeatDeuxiemePlace(resultatDto.getChevauxDeadHeatDeuxiemePlace()); + resultat.setChevauxDeadHeatTroisiemePlace(resultatDto.getChevauxDeadHeatTroisiemePlace()); + + List tousParis = parisRepository.findByCourse(course); + double totalMises = tousParis.stream().mapToDouble(ParisTrioOrdre::getMise).sum(); + double prelevementsLegaux = totalMises * 0.15; // 15% de prélèvements + double montantRembourse = calculerMontantRembourse(tousParis); + + double masseAPartager = totalMises - prelevementsLegaux - montantRembourse; + resultat.setTotalMises(totalMises); + resultat.setPrelevementsLegaux(prelevementsLegaux); + resultat.setMontantRembourse(montantRembourse); + resultat.setMasseAPartager(masseAPartager); + + traiterParis(tousParis, resultat); + + if (resultat.getMontantCagnotte() > 0) { + Cagnotte cagnotte = new Cagnotte(); + cagnotte.setMontant(resultat.getMontantCagnotte()); + cagnotte.setCourseSource(course); + cagnotteRepository.save(cagnotte); + } + + course.setEstTerminee(true); + course.setADeadHeat(resultat.hasDeadHeat()); + courseRepository.save(course); + resultatRepository.save(resultat); + } + + private double calculerMontantRembourse(List paris) { + return paris.stream() + .filter(p -> p.getPremier().isNonPartant() && + p.getDeuxieme().isNonPartant() && + p.getTroisieme().isNonPartant()) + .mapToDouble(ParisTrioOrdre::getMise) + .sum(); + } + + private void traiterParis(List paris, ResultatCourse resultat) { + for (ParisTrioOrdre p : paris) { + if (p.getPremier().isNonPartant() || p.getDeuxieme().isNonPartant() || p.getTroisieme().isNonPartant()) { + traiterParisAvecNonPartants(p, resultat); + } else if (estParisGagnant(p, resultat)) { + p.setStatut(ParisTrioOrdre.StatutParis.GAGNANT); + p.setGains(calculerGains(p, resultat)); + } else { + p.setStatut(ParisTrioOrdre.StatutParis.PERDANT); + p.setGains(0.0); + } + parisRepository.save(p); + } + } + + private boolean estParisGagnant(ParisTrioOrdre paris, ResultatCourse resultat) { + // Implémentation complexe des règles de dead heat selon l'article 3 + return paris.getPremier().equals(resultat.getPremier()) && + paris.getDeuxieme().equals(resultat.getDeuxieme()) && + paris.getTroisieme().equals(resultat.getTroisieme()); + } + + private double calculerGains(ParisTrioOrdre paris, ResultatCourse resultat) { + return Math.max(paris.getMise() * 1.1, paris.getMise() * 5.0); // Minimum 1.1 + } + + private void traiterParisAvecNonPartants(ParisTrioOrdre paris, ResultatCourse resultat) { + int nbNonPartants = compterNonPartants(paris); + + if (nbNonPartants >= 3) { + paris.setStatut(ParisTrioOrdre.StatutParis.REMBOURSE); + paris.setGains(paris.getMise()); + } else if (nbNonPartants == 2) { + if (estSpecialGagnant(paris, resultat)) { + paris.setStatut(ParisTrioOrdre.StatutParis.SPECIAL_GAGNANT); + paris.setGains(paris.getMise() * 20.0); // 20 fois la mise (1/4 du rapport TRIO-ORDRE) + } else { + paris.setStatut(ParisTrioOrdre.StatutParis.PERDANT); + paris.setGains(0.0); + } + } else if (nbNonPartants == 1) { + if (estSpecialJumele(paris, resultat)) { + paris.setStatut(ParisTrioOrdre.StatutParis.SPECIAL_JUMELE); + paris.setGains(paris.getMise() * 40.0); // 40 fois la mise (1/2 du rapport TRIO-ORDRE) + } else { + paris.setStatut(ParisTrioOrdre.StatutParis.PERDANT); + paris.setGains(0.0); + } + } + } + + private boolean estSpecialGagnant(ParisTrioOrdre paris, ResultatCourse resultat) { + Cheval chevalPartant = paris.getPremier().isNonPartant() ? + (paris.getDeuxieme().isNonPartant() ? paris.getTroisieme() : paris.getDeuxieme()) : + paris.getPremier(); + + return chevalPartant.equals(resultat.getPremier()) && + paris.getPremier().equals(chevalPartant); // Doit être désigné à la première place + } + + private boolean estSpecialJumele(ParisTrioOrdre paris, ResultatCourse resultat) { + // Vérifier qu'il y a exactement 1 cheval non-partant + if (compterNonPartants(paris) != 1) { + return false; + } + + // Identifier le cheval non-partant et les deux chevaux partants + Cheval nonPartant = null; + Cheval partant1 = null; + Cheval partant2 = null; + + if (paris.getPremier().isNonPartant()) { + nonPartant = paris.getPremier(); + partant1 = paris.getDeuxieme(); + partant2 = paris.getTroisieme(); + } else if (paris.getDeuxieme().isNonPartant()) { + nonPartant = paris.getDeuxieme(); + partant1 = paris.getPremier(); + partant2 = paris.getTroisieme(); + } else { + nonPartant = paris.getTroisieme(); + partant1 = paris.getPremier(); + partant2 = paris.getDeuxieme(); + } + + // Vérifier que les deux chevaux partants sont bien classés aux deux premières places + // ET qu'ils ont été désignés dans le bon ordre par le parieur + boolean bonOrdre = false; + + // Cas 1: partant1 est premier et partant2 est deuxième + if (partant1.equals(resultat.getPremier()) && partant2.equals(resultat.getDeuxieme())) { + // Vérifier que le parieur a bien mis partant1 en premier et partant2 en deuxième + if (paris.getPremier().equals(partant1) && paris.getDeuxieme().equals(partant2)) { + bonOrdre = true; + } + } + // Cas 2: partant2 est premier et partant1 est deuxième (dead heat possible) + else if (partant2.equals(resultat.getPremier()) && partant1.equals(resultat.getDeuxieme())) { + // Vérifier que le parieur a bien mis partant2 en premier et partant1 en deuxième + if (paris.getPremier().equals(partant2) && paris.getDeuxieme().equals(partant1)) { + bonOrdre = true; + } + } + + // Vérifier aussi pour les dead heat à la première place + if (resultat.getChevauxDeadHeatPremierePlace() != null && + resultat.getChevauxDeadHeatPremierePlace().contains(partant1.getId()) && + resultat.getChevauxDeadHeatPremierePlace().contains(partant2.getId())) { + // Les deux chevaux partants sont en dead heat à la première place + // Le parieur doit avoir mis l'un en premier et l'autre en deuxième (ordre quelconque) + if ((paris.getPremier().equals(partant1) && paris.getDeuxieme().equals(partant2)) || + (paris.getPremier().equals(partant2) && paris.getDeuxieme().equals(partant1))) { + bonOrdre = true; + } + } + + // Conditions supplémentaires: + // 1. Les deux chevaux partants doivent être dans les deux premiers à l'arrivée + // 2. Ils doivent être désignés dans l'ordre exact d'arrivée + // 3. Le cheval non-partant doit être à la troisième position dans le pari + boolean dansLesDeuxPremiers = (partant1.equals(resultat.getPremier()) || partant1.equals(resultat.getDeuxieme()) || + partant1.getId().equals(resultat.getChevauxDeadHeatPremierePlace()) || + partant1.getId().equals(resultat.getChevauxDeadHeatDeuxiemePlace())) && + (partant2.equals(resultat.getPremier()) || partant2.equals(resultat.getDeuxieme()) || + partant2.getId().equals(resultat.getChevauxDeadHeatPremierePlace()) || + partant2.getId().equals(resultat.getChevauxDeadHeatDeuxiemePlace())); + + // Vérifier que le non-partant était bien à la troisième position dans le pari + boolean nonPartantEnTroisieme = paris.getTroisieme().equals(nonPartant); + + return dansLesDeuxPremiers && bonOrdre && nonPartantEnTroisieme; + } + + private int compterNonPartants(ParisTrioOrdre paris) { + int count = 0; + if (paris.getPremier().isNonPartant()) count++; + if (paris.getDeuxieme().isNonPartant()) count++; + if (paris.getTroisieme().isNonPartant()) count++; + return count; + } + + private Cheval validerCheval(Long idCheval, String position) throws ResultatCourseInvalideException { + if (idCheval == null) { + throw new ResultatCourseInvalideException("Cheval " + position + " non spécifié"); + } + return chevalRepository.findById(idCheval) + .orElseThrow(() -> new ResultatCourseInvalideException("Cheval " + position + " non trouvé")); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/controller/CourseController.java b/src/main/java/com/pmumali/triplet/controller/CourseController.java new file mode 100644 index 0000000..2eab3f9 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/controller/CourseController.java @@ -0,0 +1,64 @@ +package com.pmumali.triplet.controller; + +import com.pmumali.triplet.model.Course; +import com.pmumali.triplet.service.CourseService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/courses") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class CourseController { + + private final CourseService courseService; + + @GetMapping + public ResponseEntity> getAllCourses() { + return ResponseEntity.ok(courseService.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getCourseById(@PathVariable Long id) { + return courseService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createCourse(@RequestBody Course course) { + return ResponseEntity.ok(courseService.save(course)); + } + + @PutMapping("/{id}") + public ResponseEntity updateCourse(@PathVariable Long id, @RequestBody Course course) { + return courseService.update(id, course) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCourse(@PathVariable Long id) { + courseService.deleteById(id); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{id}/terminer") + public ResponseEntity terminerCourse(@PathVariable Long id) { + courseService.terminerCourse(id); + return ResponseEntity.ok().build(); + } + + @GetMapping("/statut/{statut}") + public ResponseEntity> getCoursesByStatut(@PathVariable Course.StatutCourse statut) { + return ResponseEntity.ok(courseService.findByStatut(statut)); + } + + @GetMapping("/hippodrome/{hippodrome}") + public ResponseEntity> getCoursesByHippodrome(@PathVariable String hippodrome) { + return ResponseEntity.ok(courseService.findByHippodrome(hippodrome)); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/controller/PariController.java b/src/main/java/com/pmumali/triplet/controller/PariController.java new file mode 100644 index 0000000..bd05044 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/controller/PariController.java @@ -0,0 +1,83 @@ +package com.pmu.mali.triplet.controller; + +import com.pmumali.triplet.model.Pari; +import com.pmumali.triplet.service.PariService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/paris") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class PariController { + + private final PariService pariService; + + @GetMapping + public ResponseEntity> getAllParis() { + return ResponseEntity.ok(pariService.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getPariById(@PathVariable Long id) { + return pariService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createPari(@RequestBody Pari pari) { + try { + return ResponseEntity.ok(pariService.creerPari(pari)); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(null); + } + } + + @GetMapping("/course/{courseId}") + public ResponseEntity> getParisByCourse(@PathVariable Long courseId) { + return ResponseEntity.ok(pariService.findByCourseId(courseId)); + } + + @GetMapping("/utilisateur/{utilisateurId}") + public ResponseEntity> getParisByUtilisateur(@PathVariable String utilisateurId) { + return ResponseEntity.ok(pariService.findByUtilisateurId(utilisateurId)); + } + + @GetMapping("/statut/{statut}") + public ResponseEntity> getParisByStatut(@PathVariable Pari.StatutPari statut) { + return ResponseEntity.ok(pariService.findByStatut(statut)); + } + + @GetMapping("/course/{courseId}/statut/{statut}") + public ResponseEntity> getParisByCourseAndStatut( + @PathVariable Long courseId, + @PathVariable Pari.StatutPari statut) { + return ResponseEntity.ok(pariService.findByCourseIdAndStatut(courseId, statut)); + } + + @GetMapping("/course/{courseId}/total-mises") + public ResponseEntity getTotalMisesByCourse(@PathVariable Long courseId) { + return ResponseEntity.ok(pariService.getTotalMisesByCourse(courseId)); + } + + @GetMapping("/course/{courseId}/nombre-paris") + public ResponseEntity getNombreParisByCourse(@PathVariable Long courseId) { + return ResponseEntity.ok(pariService.countParisByCourse(courseId)); + } + + @PutMapping("/{id}") + public ResponseEntity updatePari(@PathVariable Long id, @RequestBody Pari pari) { + // Implémentation de la mise à jour + return ResponseEntity.ok(pariService.save(pari)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePari(@PathVariable Long id) { + pariService.deleteById(id); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/controller/ResultatController.java b/src/main/java/com/pmumali/triplet/controller/ResultatController.java new file mode 100644 index 0000000..6cae6b7 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/controller/ResultatController.java @@ -0,0 +1,47 @@ +package com.pmu.mali.triplet.controller; + +import com.pmumali.triplet.model.ResultatCourse; +import com.pmumali.triplet.service.CalculGainsService; +import com.pmumali.triplet.service.ResultatCourseService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/resultats") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class ResultatController { + + private final CalculGainsService calculGainsService; + private final ResultatCourseService resultatCourseService; + + @PostMapping("/calculer/{courseId}") + public ResponseEntity calculerResultats(@PathVariable Long courseId) { + calculGainsService.calculerResultatsCourse(courseId); + return ResponseEntity.ok().build(); + } + + @GetMapping("/course/{courseId}") + public ResponseEntity getResultatByCourse(@PathVariable Long courseId) { + return resultatCourseService.findByCourseId(courseId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/rapports/course/{courseId}") + public ResponseEntity getRapportsCourse(@PathVariable Long courseId) { + return resultatCourseService.findByCourseId(courseId) + .map(resultat -> { + String rapports = String.format( + "Ordre exact: %.2f, Désordre: %.2f, Spécial Jumelé: %.2f, Spécial Gagnant: %.2f", + resultat.getRapportTripletOrdre(), + resultat.getRapportTripletDesordre(), + resultat.getRapportSpecialJumele(), + resultat.getRapportSpecialGagnant() + ); + return ResponseEntity.ok(rapports); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/model/Cagnotte.java b/src/main/java/com/pmumali/triplet/model/Cagnotte.java new file mode 100644 index 0000000..7ae7b21 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/model/Cagnotte.java @@ -0,0 +1,47 @@ +package com.pmumali.triplet.model; + +import lombok.*; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "cagnottes") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Cagnotte { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "course_id") + private Course course; + + @Column(nullable = false) + private Double montant; + + @Column(nullable = false) + private LocalDateTime dateCreation; + + private LocalDateTime dateUtilisation; + + @Column(nullable = false) + private Boolean estUtilisee; + + @Enumerated(EnumType.STRING) + private TypeCagnotte typeCagnotte; + + private String description; + + public enum TypeCagnotte { + TRIPLET_ORDRE_EXACT, + TRIPLET_ORDRE_INEXACT, + SPECIAL_JUMELE, + SPECIAL_GAGNANT, + GENERALE + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/model/Cheval.java b/src/main/java/com/pmumali/triplet/model/Cheval.java new file mode 100644 index 0000000..84c4752 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/model/Cheval.java @@ -0,0 +1,39 @@ +package com.pmumali.triplet.model; + +import lombok.*; +import jakarta.persistence.*; + +@Entity +@Table(name = "chevaux") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Cheval { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String nom; + + @Column(nullable = false) + private Integer numero; + + @ManyToOne + @JoinColumn(name = "course_id", nullable = false) + private Course course; + + @Column(nullable = false) + private Boolean estPartant; + + private Integer positionArrivee; + + @Enumerated(EnumType.STRING) + private StatutCheval statut; + + public enum StatutCheval { + PARTANT, NON_PARTANT, ELIMINE, DEAD_HEAT + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/model/Course.java b/src/main/java/com/pmumali/triplet/model/Course.java new file mode 100644 index 0000000..f808d82 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/model/Course.java @@ -0,0 +1,44 @@ +package com.pmumali.triplet.model; + +import lombok.*; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "courses") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Course { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String nom; + + @Column(nullable = false) + private LocalDateTime dateHeure; + + @Column(nullable = false) + private String hippodrome; + + @Column(nullable = false) + private Integer nombrePartants; + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL) + private List chevaux; + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL) + private List paris; + + @Enumerated(EnumType.STRING) + private StatutCourse statut; + + public enum StatutCourse { + PROGRAMMEE, EN_COURS, TERMINEE, ANNULEE + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/model/Pari.java b/src/main/java/com/pmumali/triplet/model/Pari.java new file mode 100644 index 0000000..27bf88c --- /dev/null +++ b/src/main/java/com/pmumali/triplet/model/Pari.java @@ -0,0 +1,65 @@ +package com.pmumali.triplet.model; + +import lombok.*; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "paris") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Pari { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String numeroTicket; + + @ManyToOne + @JoinColumn(name = "course_id", nullable = false) + private Course course; + + @Column(nullable = false) + private Double mise; + + @Column(nullable = false) + private LocalDateTime dateHeurePari; + + @ElementCollection + @CollectionTable(name = "pari_chevaux_ordre", joinColumns = @JoinColumn(name = "pari_id")) + @Column(name = "cheval_id") + @OrderColumn(name = "position") + private List chevauxOrdre; + + @Enumerated(EnumType.STRING) + private TypePari typePari; + + private Double gain; + + @Enumerated(EnumType.STRING) + private StatutPari statut; + + public enum TypePari { + TRIPLET_ORDRE_EXACT, + TRIPLET_ORDRE_INEXACT, + FORMULE_COMPLETE, + FORMULE_SIMPLIFIEE, + CHAMP_TOTAL_2_CHEVAUX_COMPLET, + CHAMP_TOTAL_2_CHEVAUX_SIMPLIFIE, + CHAMP_PARTIEL_2_CHEVAUX_COMPLET, + CHAMP_PARTIEL_2_CHEVAUX_SIMPLIFIE, + CHAMP_TOTAL_1_CHEVAL_COMPLET, + CHAMP_TOTAL_1_CHEVAL_SIMPLIFIE, + CHAMP_PARTIEL_1_CHEVAL_COMPLET, + CHAMP_PARTIEL_1_CHEVAL_SIMPLIFIE + } + + public enum StatutPari { + EN_ATTENTE, GAGNANT, PERDANT, REMBOURSE, SPECIAL_JUMELE, SPECIAL_GAGNANT + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/model/ResultatCourse.java b/src/main/java/com/pmumali/triplet/model/ResultatCourse.java new file mode 100644 index 0000000..2a77887 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/model/ResultatCourse.java @@ -0,0 +1,46 @@ +package com.pmumali.triplet.model; + +import lombok.*; +import jakarta.persistence.*; +import java.util.Map; + +@Entity +@Table(name = "resultats_courses") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ResultatCourse { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "course_id", nullable = false) + private Course course; + + @ElementCollection + @CollectionTable(name = "resultat_positions", joinColumns = @JoinColumn(name = "resultat_id")) + @MapKeyColumn(name = "position") + @Column(name = "cheval_id") + private Map positionsArrivee; + + private Boolean hasDeadHeat; + + @ElementCollection + @CollectionTable(name = "dead_heat_positions", joinColumns = @JoinColumn(name = "resultat_id")) + @MapKeyColumn(name = "position") + @Column(name = "nombre_chevaux") + private Map deadHeatPositions; + + private Double massePartager; + + private Double rapportTripletOrdre; + + private Double rapportTripletDesordre; + + private Double rapportSpecialJumele; + + private Double rapportSpecialGagnant; +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/CagnotteRepository.java b/src/main/java/com/pmumali/triplet/repository/CagnotteRepository.java new file mode 100644 index 0000000..94273c6 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/CagnotteRepository.java @@ -0,0 +1,31 @@ +package com.pmumali.triplet.repository; + +import com.pmumali.triplet.model.Cagnotte; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CagnotteRepository extends JpaRepository { + + Optional findByCourseId(Long courseId); + + List findByEstUtilisee(Boolean estUtilisee); + + @Query("SELECT SUM(c.montant) FROM Cagnotte c WHERE c.estUtilisee = false") + Optional getMontantTotalCagnotte(); + + @Query("SELECT c FROM Cagnotte c WHERE c.estUtilisee = false ORDER BY c.dateCreation ASC") + List findCagnottesDisponibles(); + + @Query("SELECT c FROM Cagnotte c WHERE c.course.id = :courseId AND c.typeCagnotte = :typeCagnotte") + Optional findByCourseIdAndType(@Param("courseId") Long courseId, + @Param("typeCagnotte") Cagnotte.TypeCagnotte typeCagnotte); + + @Query("UPDATE Cagnotte c SET c.estUtilisee = true WHERE c.id = :id") + void marquerCommeUtilisee(@Param("id") Long id); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/ChevalRepository.java b/src/main/java/com/pmumali/triplet/repository/ChevalRepository.java new file mode 100644 index 0000000..4a543c2 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/ChevalRepository.java @@ -0,0 +1,33 @@ +package com.pmumali.triplet.repository; + +import com.pmumali.triplet.model.Cheval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChevalRepository extends JpaRepository { + List findByCourseId(Long courseId); + List findByNom(String nom); + List findByEstPartant(Boolean estPartant); + List findByCourseIdAndEstPartant(Long courseId, Boolean estPartant); + List findByCourseIdAndPositionArriveeIsNotNull(Long courseId); + Optional findByCourseIdAndNumero(Long courseId, Integer numero); + + @Query("SELECT c FROM Cheval c WHERE c.course.id = :courseId AND c.positionArrivee = :position") + List findByCourseIdAndPositionArrivee(@Param("courseId") Long courseId, + @Param("position") Integer position); + + @Query("SELECT c FROM Cheval c WHERE c.course.id = :courseId AND c.statut = 'DEAD_HEAT'") + List findDeadHeatByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT COUNT(c) FROM Cheval c WHERE c.course.id = :courseId AND c.estPartant = true") + long countPartantsByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT c FROM Cheval c WHERE c.course.id = :courseId ORDER BY c.positionArrivee ASC") + List findClassementByCourseId(@Param("courseId") Long courseId); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/CourseRepository.java b/src/main/java/com/pmumali/triplet/repository/CourseRepository.java new file mode 100644 index 0000000..73b09ee --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/CourseRepository.java @@ -0,0 +1,16 @@ +package com.pmumali.triplet.repository; + +import com.pmumali.triplet.model.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CourseRepository extends JpaRepository { + List findByStatut(Course.StatutCourse statut); + List findByHippodrome(String hippodrome); + boolean existsByNom(String nom); + List findByOrderByDateHeureAsc(); + List findByDateHeureAfter(java.time.LocalDateTime dateHeure); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/PariRepository.java b/src/main/java/com/pmumali/triplet/repository/PariRepository.java new file mode 100644 index 0000000..725d0aa --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/PariRepository.java @@ -0,0 +1,53 @@ +package com.pmumali.triplet.repository; + +import com.pmumali.triplet.model.Pari; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PariRepository extends JpaRepository { + List findByCourseId(Long courseId); + List findByUtilisateurId(String utilisateurId); + List findByStatut(Pari.StatutPari statut); + List findByCourseIdAndStatut(Long courseId, Pari.StatutPari statut); + List findByTypePari(Pari.TypePari typePari); + List findByCourseIdAndTypePari(Long courseId, Pari.TypePari typePari); + + @Query("SELECT p FROM Pari p WHERE p.course.id = :courseId AND p.statut = 'GAGNANT'") + List findGagnantsByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT p FROM Pari p WHERE p.course.id = :courseId AND p.statut IN ('REMBOURSE', 'SPECIAL_JUMELE', 'SPECIAL_GAGNANT')") + List findSpeciauxByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT SUM(p.mise) FROM Pari p WHERE p.course.id = :courseId") + Double sumMisesByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT SUM(p.mise) FROM Pari p WHERE p.course.id = :courseId AND p.statut = 'REMBOURSE'") + Double sumMisesRembourseesByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT COUNT(p) FROM Pari p WHERE p.course.id = :courseId") + long countByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT COUNT(p) FROM Pari p WHERE p.course.id = :courseId AND p.typePari = :typePari") + long countByCourseIdAndTypePari(@Param("courseId") Long courseId, + @Param("typePari") Pari.TypePari typePari); + + @Query("SELECT SUM(p.mise) FROM Pari p WHERE p.course.id = :courseId AND p.typePari = :typePari") + Double sumMisesByCourseIdAndTypePari(@Param("courseId") Long courseId, + @Param("typePari") Pari.TypePari typePari); + + @Query("SELECT p FROM Pari p WHERE p.numeroTicket = :numeroTicket") + Optional findByNumeroTicket(@Param("numeroTicket") String numeroTicket); + + @Query("SELECT p FROM Pari p WHERE p.course.id = :courseId AND :chevalId MEMBER OF p.chevauxOrdre") + List findByCourseIdAndChevalInPari(@Param("courseId") Long courseId, + @Param("chevalId") Long chevalId); + + @Query("SELECT p FROM Pari p WHERE p.utilisateurId = :utilisateurId AND p.statut = 'GAGNANT' ORDER BY p.gain DESC") + List findGagnantsByUtilisateurId(@Param("utilisateurId") String utilisateurId); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/ResultatCourseRepository.java b/src/main/java/com/pmumali/triplet/repository/ResultatCourseRepository.java new file mode 100644 index 0000000..97d867a --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/ResultatCourseRepository.java @@ -0,0 +1,35 @@ +package com.pmumali.triplet.repository; + +import com.pmumali.triplet.model.ResultatCourse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ResultatCourseRepository extends JpaRepository { + Optional findByCourseId(Long courseId); + + @Query("SELECT r FROM ResultatCourse r WHERE r.course.id = :courseId") + Optional findByCourse(@Param("courseId") Long courseId); + + @Query("SELECT CASE WHEN COUNT(r) > 0 THEN true ELSE false END FROM ResultatCourse r WHERE r.course.id = :courseId") + boolean existsByCourseId(@Param("courseId") Long courseId); + + @Query("DELETE FROM ResultatCourse r WHERE r.course.id = :courseId") + void deleteByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT r.rapportTripletOrdre FROM ResultatCourse r WHERE r.course.id = :courseId") + Optional findRapportOrdreByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT r.rapportTripletDesordre FROM ResultatCourse r WHERE r.course.id = :courseId") + Optional findRapportDesordreByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT r.rapportSpecialJumele FROM ResultatCourse r WHERE r.course.id = :courseId") + Optional findRapportSpecialJumeleByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT r.rapportSpecialGagnant FROM ResultatCourse r WHERE r.course.id = :courseId") + Optional findRapportSpecialGagnantByCourseId(@Param("courseId") Long courseId); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/repository/StatistiqueRepository.java b/src/main/java/com/pmumali/triplet/repository/StatistiqueRepository.java new file mode 100644 index 0000000..affcea5 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/repository/StatistiqueRepository.java @@ -0,0 +1,84 @@ +package com.pmumali.triplet.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Repository +public interface StatistiqueRepository extends JpaRepository { + + @Query(value = """ + SELECT DATE(p.date_heure_pari) as date, + COUNT(p.id) as nombre_paris, + SUM(p.mise) as total_mises, + SUM(p.gain) as total_gains + FROM paris p + WHERE p.date_heure_pari BETWEEN :startDate AND :endDate + GROUP BY DATE(p.date_heure_pari) + ORDER BY date + """, nativeQuery = true) + List getStatistiquesJournalieres(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query(value = """ + SELECT c.hippodrome, + COUNT(p.id) as nombre_paris, + SUM(p.mise) as total_mises, + SUM(p.gain) as total_gains + FROM paris p + JOIN courses c ON p.course_id = c.id + WHERE c.date_heure BETWEEN :startDate AND :endDate + GROUP BY c.hippodrome + ORDER BY total_mises DESC + """, nativeQuery = true) + List getStatistiquesParHippodrome(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query(value = """ + SELECT p.type_pari, + COUNT(p.id) as nombre_paris, + SUM(p.mise) as total_mises, + AVG(p.mise) as mise_moyenne + FROM paris p + WHERE p.date_heure_pari BETWEEN :startDate AND :endDate + GROUP BY p.type_pari + ORDER BY total_mises DESC + """, nativeQuery = true) + List getStatistiquesParTypePari(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query(value = """ + SELECT p.statut, + COUNT(p.id) as nombre_paris, + SUM(p.mise) as total_mises, + SUM(p.gain) as total_gains + FROM paris p + WHERE p.date_heure_pari BETWEEN :startDate AND :endDate + GROUP BY p.statut + ORDER BY total_gains DESC + """, nativeQuery = true) + List getStatistiquesParStatut(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query(value = """ + SELECT c.nom as course, + COUNT(p.id) as nombre_paris, + SUM(p.mise) as total_mises, + r.rapport_triplet_ordre as rapport_ordre, + r.rapport_triplet_desordre as rapport_desordre + FROM paris p + JOIN courses c ON p.course_id = c.id + LEFT JOIN resultats_courses r ON c.id = r.course_id + WHERE c.date_heure BETWEEN :startDate AND :endDate + GROUP BY c.id, c.nom, r.rapport_triplet_ordre, r.rapport_triplet_desordre + ORDER BY total_mises DESC + LIMIT 10 + """, nativeQuery = true) + List getTopCoursesParMises(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/CagnotteService.java b/src/main/java/com/pmumali/triplet/service/CagnotteService.java new file mode 100644 index 0000000..5dc4b48 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/CagnotteService.java @@ -0,0 +1,79 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.Cagnotte; +import com.pmumali.triplet.model.Course; +import com.pmumali.triplet.repository.CagnotteRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CagnotteService { + + private final CagnotteRepository cagnotteRepository; + + public Cagnotte creerCagnotte(Course course, Double montant, Cagnotte.TypeCagnotte type, String description) { + Cagnotte cagnotte = Cagnotte.builder() + .course(course) + .montant(montant) + .dateCreation(LocalDateTime.now()) + .estUtilisee(false) + .typeCagnotte(type) + .description(description) + .build(); + + return cagnotteRepository.save(cagnotte); + } + + public Optional getCagnotteByCourse(Long courseId) { + return cagnotteRepository.findByCourseId(courseId); + } + + public List getCagnottesDisponibles() { + return cagnotteRepository.findCagnottesDisponibles(); + } + + public Double getMontantTotalCagnotte() { + return cagnotteRepository.getMontantTotalCagnotte().orElse(0.0); + } + + public void utiliserCagnotte(Long cagnotteId, Long courseId) { + cagnotteRepository.findById(cagnotteId).ifPresent(cagnotte -> { + cagnotte.setEstUtilisee(true); + cagnotte.setDateUtilisation(LocalDateTime.now()); + cagnotteRepository.save(cagnotte); + }); + } + + public void transfererCagnotteVersCourse(Long courseId) { + List cagnottesDisponibles = getCagnottesDisponibles(); + double montantTotal = cagnottesDisponibles.stream() + .mapToDouble(Cagnotte::getMontant) + .sum(); + + // Marquer toutes les cagnottes comme utilisées + cagnottesDisponibles.forEach(cagnotte -> { + cagnotte.setEstUtilisee(true); + cagnotte.setDateUtilisation(LocalDateTime.now()); + cagnotteRepository.save(cagnotte); + }); + + // Créer une nouvelle cagnotte pour la course avec le montant total + if (montantTotal > 0) { + // Implémentation simplifiée - en réalité, il faudrait récupérer la course + Cagnotte nouvelleCagnotte = Cagnotte.builder() + .montant(montantTotal) + .dateCreation(LocalDateTime.now()) + .estUtilisee(false) + .typeCagnotte(Cagnotte.TypeCagnotte.GENERALE) + .description("Cagnotte reportée des courses précédentes") + .build(); + + cagnotteRepository.save(nouvelleCagnotte); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/CalculGainsService.java b/src/main/java/com/pmumali/triplet/service/CalculGainsService.java new file mode 100644 index 0000000..a91157b --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/CalculGainsService.java @@ -0,0 +1,450 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.*; +import com.pmumali.triplet.repository.ChevalRepository; +import com.pmumali.triplet.repository.PariRepository; +import com.pmumali.triplet.repository.ResultatCourseRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CalculGainsService { + + private final PariRepository pariRepository; + private final ResultatCourseRepository resultatCourseRepository; + private final ChevalRepository chevalRepository; + private final CourseService courseService; + + private static final double MISE_UNITAIRE = 500.0; + private static final double PRELEVEMENTS_POURCENTAGE = 0.15; + + public void calculerResultatsCourse(Long courseId) { + Course course = courseService.findById(courseId) + .orElseThrow(() -> new RuntimeException("Course non trouvée avec l'id: " + courseId)); + + ResultatCourse resultat = resultatCourseRepository.findByCourse(course.getId()) + .orElseGet(() -> creerResultatCourse(course)); + + // Calcul de la masse à partager selon l'article 5 + double massePartager = calculerMassePartager(course); + resultat.setMassePartager(massePartager); + + // Déterminer le type d'arrivée + boolean hasDeadHeat = resultat.getHasDeadHeat(); + Map deadHeatInfo = resultat.getDeadHeatPositions(); + + // Calcul des rapports selon le type d'arrivée + if (hasDeadHeat) { + calculerRapportsAvecDeadHeat(resultat, deadHeatInfo, massePartager); + } else { + calculerRapportsArriveeNormale(resultat, massePartager); + } + + resultatCourseRepository.save(resultat); + + // Calculer les gains pour tous les paris + calculerGainsParis(course, resultat); + } + + private ResultatCourse creerResultatCourse(Course course) { + ResultatCourse resultat = new ResultatCourse(); + resultat.setCourse(course); + resultat.setHasDeadHeat(determinerDeadHeat(course)); + resultat.setDeadHeatPositions(analyserDeadHeatPositions(course)); + resultat.setPositionsArrivee(determinerPositionsArrivee(course)); + return resultatCourseRepository.save(resultat); + } + + private double calculerMassePartager(Course course) { + double recetteNette = calculerRecetteNette(course); + double montantRembourses = calculerMontantRembourses(course); + double prelevementsLegaux = calculerPrelevementsLegaux(recetteNette); + + return recetteNette - montantRembourses - prelevementsLegaux; + } + + private double calculerRecetteNette(Course course) { + List paris = pariRepository.findByCourseId(course.getId()); + return paris.stream() + .filter(p -> p.getStatut() != Pari.StatutPari.REMBOURSE) + .mapToDouble(Pari::getMise) + .sum(); + } + + private double calculerMontantRembourses(Course course) { + List paris = pariRepository.findByCourseId(course.getId()); + return paris.stream() + .filter(p -> p.getStatut() == Pari.StatutPari.REMBOURSE) + .mapToDouble(Pari::getMise) + .sum(); + } + + private double calculerPrelevementsLegaux(double recetteNette) { + return recetteNette * PRELEVEMENTS_POURCENTAGE; + } + + private void calculerRapportsArriveeNormale(ResultatCourse resultat, double massePartager) { + Course course = resultat.getCourse(); + + // 60% pour l'ordre exact, 40% pour l'ordre inexact selon l'article 5 + double masseOrdreExact = massePartager * 0.6; + double masseOrdreInexact = massePartager * 0.4; + + // Calcul des mises pour chaque type + long misesOrdreExact = compterMisesOrdreExact(course); + long misesOrdreInexact = compterMisesOrdreInexact(course); + + // Rapports bruts + double rapportOrdreExact = misesOrdreExact > 0 ? (masseOrdreExact / misesOrdreExact) / MISE_UNITAIRE : 0; + double rapportOrdreInexact = misesOrdreInexact > 0 ? (masseOrdreInexact / misesOrdreInexact) / MISE_UNITAIRE : 0; + + // Application de la proportion minimum (article 6a) + if (rapportOrdreExact < 5 * rapportOrdreInexact) { + double totalMisesPondere = 5 * misesOrdreExact + misesOrdreInexact; + double rapportBase = massePartager / totalMisesPondere / MISE_UNITAIRE; + rapportOrdreInexact = rapportBase; + rapportOrdreExact = 5 * rapportBase; + } + + // Rapports minimum (article 5) + rapportOrdreExact = Math.max(rapportOrdreExact, 1.1); + rapportOrdreInexact = Math.max(rapportOrdreInexact, 1.1); + + resultat.setRapportTripletOrdre(rapportOrdreExact); + resultat.setRapportTripletDesordre(rapportOrdreInexact); + resultat.setRapportSpecialJumele(rapportOrdreInexact / 2); + resultat.setRapportSpecialGagnant(rapportOrdreInexact / 4); + } + + private void calculerRapportsAvecDeadHeat(ResultatCourse resultat, + Map deadHeatInfo, + double massePartager) { + + if (deadHeatInfo.containsKey(1) && deadHeatInfo.get(1) >= 3) { + calculerDeadHeatPremierePlace(resultat, deadHeatInfo, massePartager); + } else if (deadHeatInfo.containsKey(1) && deadHeatInfo.get(1) == 2 && deadHeatInfo.containsKey(3)) { + calculerDeadHeatPremiereEtTroisieme(resultat, deadHeatInfo, massePartager); + } else if (deadHeatInfo.containsKey(2) && deadHeatInfo.get(2) >= 2) { + calculerDeadHeatDeuxiemePlace(resultat, deadHeatInfo, massePartager); + } else if (deadHeatInfo.containsKey(3) && deadHeatInfo.get(3) >= 2) { + calculerDeadHeatTroisiemePlace(resultat, deadHeatInfo, massePartager); + } + } + + private void calculerDeadHeatPremierePlace(ResultatCourse resultat, + Map deadHeatInfo, + double massePartager) { + int nombreCombinaisonsPayables = calculerNombreCombinaisonsPremierePlace(deadHeatInfo); + double beneficeParCombinaison = massePartager / nombreCombinaisonsPayables; + + // Pour chaque combinaison, répartir le bénéfice entre les 6 permutations + double rapportParCombinaison = beneficeParCombinaison / MISE_UNITAIRE; + + resultat.setRapportTripletOrdre(rapportParCombinaison); + resultat.setRapportTripletDesordre(rapportParCombinaison); + resultat.setRapportSpecialJumele(rapportParCombinaison / 2); + resultat.setRapportSpecialGagnant(rapportParCombinaison / 4); + } + + private void calculerDeadHeatPremiereEtTroisieme(ResultatCourse resultat, + Map deadHeatInfo, + double massePartager) { + int nombreCombinaisons = deadHeatInfo.get(1) * deadHeatInfo.get(3); + double beneficeParCombinaison = massePartager / nombreCombinaisons; + + // Pour chaque combinaison: 50% pour ordre exact (2 permutations), 50% pour ordre inexact (4 permutations) + double masseOrdreExact = beneficeParCombinaison * 0.5; + double masseOrdreInexact = beneficeParCombinaison * 0.5; + + long misesOrdreExact = compterMisesOrdreExact(resultat.getCourse()); + long misesOrdreInexact = compterMisesOrdreInexact(resultat.getCourse()); + + double rapportOrdreExact = misesOrdreExact > 0 ? (masseOrdreExact / misesOrdreExact) / MISE_UNITAIRE : 0; + double rapportOrdreInexact = misesOrdreInexact > 0 ? (masseOrdreInexact / misesOrdreInexact) / MISE_UNITAIRE : 0; + + // Application proportion minimum (article 6c) + if (rapportOrdreExact < 2 * rapportOrdreInexact) { + double totalMisesPondere = 2 * misesOrdreExact + misesOrdreInexact; + double rapportBase = beneficeParCombinaison / totalMisesPondere / MISE_UNITAIRE; + rapportOrdreInexact = rapportBase; + rapportOrdreExact = 2 * rapportBase; + } + + rapportOrdreExact = Math.max(rapportOrdreExact, 1.1); + rapportOrdreInexact = Math.max(rapportOrdreInexact, 1.1); + + resultat.setRapportTripletOrdre(rapportOrdreExact); + resultat.setRapportTripletDesordre(rapportOrdreInexact); + resultat.setRapportSpecialJumele(rapportOrdreInexact / 2); + resultat.setRapportSpecialGagnant(rapportOrdreInexact / 4); + } + + private void calculerDeadHeatDeuxiemePlace(ResultatCourse resultat, + Map deadHeatInfo, + double massePartager) { + int nombreCombinaisons = deadHeatInfo.get(2); + double beneficeParCombinaison = massePartager / nombreCombinaisons; + + // Même logique que pour premier et troisième + double masseOrdreExact = beneficeParCombinaison * 0.5; + double masseOrdreInexact = beneficeParCombinaison * 0.5; + + long misesOrdreExact = compterMisesOrdreExact(resultat.getCourse()); + long misesOrdreInexact = compterMisesOrdreInexact(resultat.getCourse()); + + double rapportOrdreExact = misesOrdreExact > 0 ? (masseOrdreExact / misesOrdreExact) / MISE_UNITAIRE : 0; + double rapportOrdreInexact = misesOrdreInexact > 0 ? (masseOrdreInexact / misesOrdreInexact) / MISE_UNITAIRE : 0; + + // Application proportion minimum (article 6c) + if (rapportOrdreExact < 2 * rapportOrdreInexact) { + double totalMisesPondere = 2 * misesOrdreExact + misesOrdreInexact; + double rapportBase = beneficeParCombinaison / totalMisesPondere / MISE_UNITAIRE; + rapportOrdreInexact = rapportBase; + rapportOrdreExact = 2 * rapportBase; + } + + rapportOrdreExact = Math.max(rapportOrdreExact, 1.1); + rapportOrdreInexact = Math.max(rapportOrdreInexact, 1.1); + + resultat.setRapportTripletOrdre(rapportOrdreExact); + resultat.setRapportTripletDesordre(rapportOrdreInexact); + resultat.setRapportSpecialJumele(rapportOrdreInexact / 2); + resultat.setRapportSpecialGagnant(rapportOrdreInexact / 4); + } + + private void calculerDeadHeatTroisiemePlace(ResultatCourse resultat, + Map deadHeatInfo, + double massePartager) { + int nombreCombinaisons = deadHeatInfo.get(3); + double beneficeParCombinaison = massePartager / nombreCombinaisons; + + // Pour chaque combinaison: 50% pour ordre exact (1 permutation), 50% pour ordre inexact (5 permutations) + double masseOrdreExact = beneficeParCombinaison * 0.5; + double masseOrdreInexact = beneficeParCombinaison * 0.5; + + long misesOrdreExact = compterMisesOrdreExact(resultat.getCourse()); + long misesOrdreInexact = compterMisesOrdreInexact(resultat.getCourse()); + + double rapportOrdreExact = misesOrdreExact > 0 ? (masseOrdreExact / misesOrdreExact) / MISE_UNITAIRE : 0; + double rapportOrdreInexact = misesOrdreInexact > 0 ? (masseOrdreInexact / misesOrdreInexact) / MISE_UNITAIRE : 0; + + // Application proportion minimum (article 6b) + if (rapportOrdreExact < 5 * rapportOrdreInexact) { + double totalMisesPondere = 5 * misesOrdreExact + misesOrdreInexact; + double rapportBase = beneficeParCombinaison / totalMisesPondere / MISE_UNITAIRE; + rapportOrdreInexact = rapportBase; + rapportOrdreExact = 5 * rapportBase; + } + + rapportOrdreExact = Math.max(rapportOrdreExact, 1.1); + rapportOrdreInexact = Math.max(rapportOrdreInexact, 1.1); + + resultat.setRapportTripletOrdre(rapportOrdreExact); + resultat.setRapportTripletDesordre(rapportOrdreInexact); + resultat.setRapportSpecialJumele(rapportOrdreInexact / 2); + resultat.setRapportSpecialGagnant(rapportOrdreInexact / 4); + } + + private void calculerGainsParis(Course course, ResultatCourse resultat) { + List paris = pariRepository.findByCourseId(course.getId()); + + for (Pari pari : paris) { + Pari.StatutPari statut = determinerStatutPari(pari, resultat); + pari.setStatut(statut); + + double gain = 0.0; + switch (statut) { + case GAGNANT: + gain = calculerGainPariGagnant(pari, resultat); + break; + case SPECIAL_JUMELE: + gain = resultat.getRapportSpecialJumele() * (pari.getMise() / MISE_UNITAIRE); + break; + case SPECIAL_GAGNANT: + gain = resultat.getRapportSpecialGagnant() * (pari.getMise() / MISE_UNITAIRE); + break; + case REMBOURSE: + gain = pari.getMise(); + break; + default: + gain = 0.0; + } + + pari.setGain(gain); + pariRepository.save(pari); + } + } + + private Pari.StatutPari determinerStatutPari(Pari pari, ResultatCourse resultat) { + int nombreNonPartants = compterNonPartants(pari); + + if (nombreNonPartants == 3) { + return Pari.StatutPari.REMBOURSE; + } + + List troisPremiers = getTroisPremiersChevaux(resultat); + List chevauxPari = pari.getChevauxOrdre(); + + if (!troisPremiers.containsAll(chevauxPari)) { + return Pari.StatutPari.PERDANT; + } + + if (nombreNonPartants == 1) { + return verifierSpecialJumele(pari, resultat); + } else if (nombreNonPartants == 2) { + return verifierSpecialGagnant(pari, resultat); + } + + if (pari.getTypePari() == Pari.TypePari.TRIPLET_ORDRE_EXACT) { + return estOrdreExact(pari, resultat) ? Pari.StatutPari.GAGNANT : Pari.StatutPari.PERDANT; + } + + return Pari.StatutPari.GAGNANT; + } + + private Pari.StatutPari verifierSpecialJumele(Pari pari, ResultatCourse resultat) { + List chevauxPartants = getChevauxPartants(pari); + List deuxPremiers = getDeuxPremiersChevaux(resultat); + + if (chevauxPartants.size() == 2 && deuxPremiers.containsAll(chevauxPartants)) { + // Vérifier l'ordre pour les deux chevaux partants + List ordreArrivee = getOrdreArrivee(resultat); + List ordrePari = pari.getChevauxOrdre().stream() + .filter(chevauxPartants::contains) + .collect(Collectors.toList()); + + if (ordreArrivee.get(0).equals(ordrePari.get(0)) && + ordreArrivee.get(1).equals(ordrePari.get(1))) { + return Pari.StatutPari.SPECIAL_JUMELE; + } + } + return Pari.StatutPari.PERDANT; + } + + private Pari.StatutPari verifierSpecialGagnant(Pari pari, ResultatCourse resultat) { + List chevauxPartants = getChevauxPartants(pari); + + if (chevauxPartants.size() == 1) { + Long premierCheval = getPremierCheval(resultat); + Long chevalPari = chevauxPartants.get(0); + + if (premierCheval.equals(chevalPari)) { + // Vérifier que le cheval est bien désigné à la première place + if (pari.getChevauxOrdre().get(0).equals(chevalPari)) { + return Pari.StatutPari.SPECIAL_GAGNANT; + } + } + } + return Pari.StatutPari.PERDANT; + } + + // Méthodes auxiliaires + private long compterMisesOrdreExact(Course course) { + return (long) pariRepository.findByCourseIdAndTypePari(course.getId(), Pari.TypePari.TRIPLET_ORDRE_EXACT) + .stream() + .mapToDouble(p -> p.getMise() / MISE_UNITAIRE) + .sum(); + } + + private long compterMisesOrdreInexact(Course course) { + return (long) pariRepository.findByCourseIdAndTypePari(course.getId(), Pari.TypePari.TRIPLET_ORDRE_INEXACT) + .stream() + .mapToDouble(p -> p.getMise() / MISE_UNITAIRE) + .sum(); + } + + private int compterNonPartants(Pari pari) { + return (int) pari.getChevauxOrdre().stream() + .map(chevalId -> chevalRepository.findById(chevalId).orElse(null)) + .filter(cheval -> cheval != null && !cheval.getEstPartant()) + .count(); + } + + private List getChevauxPartants(Pari pari) { + return pari.getChevauxOrdre().stream() + .map(chevalId -> chevalRepository.findById(chevalId).orElse(null)) + .filter(cheval -> cheval != null && cheval.getEstPartant()) + .map(Cheval::getId) + .collect(Collectors.toList()); + } + + private List getTroisPremiersChevaux(ResultatCourse resultat) { + return Arrays.asList( + resultat.getPositionsArrivee().get(1), + resultat.getPositionsArrivee().get(2), + resultat.getPositionsArrivee().get(3) + ); + } + + private List getDeuxPremiersChevaux(ResultatCourse resultat) { + return Arrays.asList( + resultat.getPositionsArrivee().get(1), + resultat.getPositionsArrivee().get(2) + ); + } + + private Long getPremierCheval(ResultatCourse resultat) { + return resultat.getPositionsArrivee().get(1); + } + + private List getOrdreArrivee(ResultatCourse resultat) { + return resultat.getPositionsArrivee().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + } + + private boolean estOrdreExact(Pari pari, ResultatCourse resultat) { + List ordreArrivee = getOrdreArrivee(resultat); + return pari.getChevauxOrdre().equals(ordreArrivee.subList(0, 3)); + } + + private double calculerGainPariGagnant(Pari pari, ResultatCourse resultat) { + double rapport = (pari.getTypePari() == Pari.TypePari.TRIPLET_ORDRE_EXACT) ? + resultat.getRapportTripletOrdre() : resultat.getRapportTripletDesordre(); + + return rapport * (pari.getMise() / MISE_UNITAIRE); + } + + private int calculerNombreCombinaisonsPremierePlace(Map deadHeatInfo) { + int n = deadHeatInfo.get(1); + // Combinaisons de n chevaux pris 3 à 3 + return (n * (n - 1) * (n - 2)) / 6; + } + + private boolean determinerDeadHeat(Course course) { + List chevaux = chevalRepository.findByCourseId(course.getId()); + return chevaux.stream() + .anyMatch(cheval -> cheval.getStatut() == Cheval.StatutCheval.DEAD_HEAT); + } + + private Map analyserDeadHeatPositions(Course course) { + Map deadHeatPositions = new HashMap<>(); + List chevaux = chevalRepository.findByCourseId(course.getId()); + + for (Cheval cheval : chevaux) { + if (cheval.getStatut() == Cheval.StatutCheval.DEAD_HEAT && cheval.getPositionArrivee() != null) { + deadHeatPositions.merge(cheval.getPositionArrivee(), 1, Integer::sum); + } + } + + return deadHeatPositions; + } + + private Map determinerPositionsArrivee(Course course) { + Map positions = new HashMap<>(); + List chevaux = chevalRepository.findByCourseId(course.getId()); + + for (Cheval cheval : chevaux) { + if (cheval.getPositionArrivee() != null) { + positions.put(cheval.getPositionArrivee(), cheval.getId()); + } + } + + return positions; + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/CourseService.java b/src/main/java/com/pmumali/triplet/service/CourseService.java new file mode 100644 index 0000000..dbc20f9 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/CourseService.java @@ -0,0 +1,58 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.Course; +import com.pmumali.triplet.repository.CourseRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CourseService { + + private final CourseRepository courseRepository; + + public List findAll() { + return courseRepository.findAll(); + } + + public Optional findById(Long id) { + return courseRepository.findById(id); + } + + public Course save(Course course) { + return courseRepository.save(course); + } + + public Optional update(Long id, Course courseDetails) { + return courseRepository.findById(id).map(course -> { + course.setNom(courseDetails.getNom()); + course.setDateHeure(courseDetails.getDateHeure()); + course.setHippodrome(courseDetails.getHippodrome()); + course.setNombrePartants(courseDetails.getNombrePartants()); + course.setStatut(courseDetails.getStatut()); + return courseRepository.save(course); + }); + } + + public void deleteById(Long id) { + courseRepository.deleteById(id); + } + + public void terminerCourse(Long id) { + courseRepository.findById(id).ifPresent(course -> { + course.setStatut(Course.StatutCourse.TERMINEE); + courseRepository.save(course); + }); + } + + public List findByStatut(Course.StatutCourse statut) { + return courseRepository.findByStatut(statut); + } + + public List findByHippodrome(String hippodrome) { + return courseRepository.findByHippodrome(hippodrome); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/PariService.java b/src/main/java/com/pmumali/triplet/service/PariService.java new file mode 100644 index 0000000..1d1813c --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/PariService.java @@ -0,0 +1,75 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.Pari; +import com.pmumali.triplet.repository.PariRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class PariService { + + private final PariRepository pariRepository; + private final ValidationPariService validationPariService; + + public List findAll() { + return pariRepository.findAll(); + } + + public Optional findById(Long id) { + return pariRepository.findById(id); + } + + public List findByCourseId(Long courseId) { + return pariRepository.findByCourseId(courseId); + } + + public List findByUtilisateurId(String utilisateurId) { + return pariRepository.findByUtilisateurId(utilisateurId); + } + + public Pari creerPari(Pari pari) { + // Valider le pari avant sauvegarde + validationPariService.validerPari(pari); + + // Générer un numéro de ticket unique + String numeroTicket = genererNumeroTicket(); + pari.setNumeroTicket(numeroTicket); + + return pariRepository.save(pari); + } + + public Pari save(Pari pari) { + return pariRepository.save(pari); + } + + public void deleteById(Long id) { + pariRepository.deleteById(id); + } + + public List findByStatut(Pari.StatutPari statut) { + return pariRepository.findByStatut(statut); + } + + public List findByCourseIdAndStatut(Long courseId, Pari.StatutPari statut) { + return pariRepository.findByCourseIdAndStatut(courseId, statut); + } + + public double getTotalMisesByCourse(Long courseId) { + return pariRepository.findByCourseId(courseId).stream() + .mapToDouble(Pari::getMise) + .sum(); + } + + public long countParisByCourse(Long courseId) { + return pariRepository.countByCourseId(courseId); + } + + private String genererNumeroTicket() { + // Générer un numéro de ticket unique basé sur le timestamp + return "TKT" + System.currentTimeMillis() + (int)(Math.random() * 1000); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/ResultatCourseService.java b/src/main/java/com/pmumali/triplet/service/ResultatCourseService.java new file mode 100644 index 0000000..394f281 --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/ResultatCourseService.java @@ -0,0 +1,27 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.ResultatCourse; +import com.pmumali.triplet.repository.ResultatCourseRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ResultatCourseService { + + private final ResultatCourseRepository resultatCourseRepository; + + public Optional findByCourseId(Long courseId) { + return resultatCourseRepository.findByCourseId(courseId); + } + + public ResultatCourse save(ResultatCourse resultatCourse) { + return resultatCourseRepository.save(resultatCourse); + } + + public void deleteByCourseId(Long courseId) { + resultatCourseRepository.deleteByCourseId(courseId); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmumali/triplet/service/ValidationPariService.java b/src/main/java/com/pmumali/triplet/service/ValidationPariService.java new file mode 100644 index 0000000..40a682d --- /dev/null +++ b/src/main/java/com/pmumali/triplet/service/ValidationPariService.java @@ -0,0 +1,79 @@ +package com.pmumali.triplet.service; + +import com.pmumali.triplet.model.Course; +import com.pmumali.triplet.model.Pari; +import com.pmumali.triplet.repository.ChevalRepository; +import com.pmumali.triplet.repository.CourseRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ValidationPariService { + + private final CourseRepository courseRepository; + private final ChevalRepository chevalRepository; + + public void validerPari(Pari pari) { + // Vérifier que la course existe et est ouverte aux paris + validerCourse(pari.getCourse().getId()); + + // Vérifier les chevaux + validerChevaux(pari); + + // Vérifier la mise selon l'article 2 + validerMise(pari); + + // Vérifier le type de pari et la formule + validerTypePari(pari); + } + + private void validerCourse(Long courseId) { + courseRepository.findById(courseId).ifPresentOrElse(course -> { + if (course.getStatut() != Course.StatutCourse.PROGRAMMEE) { + throw new IllegalArgumentException("La course n'est plus ouverte aux paris"); + } + }, () -> { + throw new IllegalArgumentException("Course non trouvée"); + }); + } + + private void validerChevaux(Pari pari) { + for (Long chevalId : pari.getChevauxOrdre()) { + chevalRepository.findById(chevalId).ifPresentOrElse(cheval -> { + if (!cheval.getEstPartant()) { + throw new IllegalArgumentException("Le cheval " + cheval.getNom() + " est non-partant"); + } + if (!cheval.getCourse().getId().equals(pari.getCourse().getId())) { + throw new IllegalArgumentException("Le cheval " + cheval.getNom() + + " ne participe pas à cette course"); + } + }, () -> { + throw new IllegalArgumentException("Cheval non trouvé: " + chevalId); + }); + } + } + + private void validerMise(Pari pari) { + double miseUnitaire = 500; // Mise de base selon l'article 1 + + if (pari.getMise() < miseUnitaire) { + throw new IllegalArgumentException("La mise doit être au moins " + miseUnitaire + " FCFA"); + } + + if (pari.getMise() % miseUnitaire != 0) { + throw new IllegalArgumentException("La mise doit être un multiple de " + miseUnitaire + " FCFA"); + } + + // Vérification de la limitation d'enjeu (article 2) + double enjeuMax = 20 * miseUnitaire; + if (pari.getMise() > enjeuMax) { + throw new IllegalArgumentException("L'enjeu maximum est de " + enjeuMax + " FCFA"); + } + } + + private void validerTypePari(Pari pari) { + // Validation selon le type de pari et les formules (article 7) + // Implémentation simplifiée + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..97e0406 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=API-PLR diff --git a/src/test/java/com/pmu/mali/apiplr/ApiPlrApplicationTests.java b/src/test/java/com/pmu/mali/apiplr/ApiPlrApplicationTests.java new file mode 100644 index 0000000..fbf6310 --- /dev/null +++ b/src/test/java/com/pmu/mali/apiplr/ApiPlrApplicationTests.java @@ -0,0 +1,13 @@ +package com.pmu.mali.apiplr; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApiPlrApplicationTests { + + @Test + void contextLoads() { + } + +}