From 69b0fad8e87791c572c64ad0b9c2f0d17cfa368f Mon Sep 17 00:00:00 2001 From: sidibe Date: Mon, 25 Aug 2025 18:26:02 +0000 Subject: [PATCH] Initial commit --- .gitattributes | 3 + .gitignore | 37 ++ build.gradle | 38 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++++++++ gradlew.bat | 94 +++ settings.gradle | 1 + .../jumele/controller/JumeleController.java | 40 ++ .../com/pmu/jumele/dto/PaiementResponse.java | 14 + .../java/com/pmu/jumele/dto/PariRequest.java | 14 + .../com/pmu/jumele/dto/PositionsRequest.java | 14 + .../com/pmu/jumele/entity/PariEntity.java | 24 + .../pmu/jumele/repository/PariRepository.java | 10 + .../jumele/service/JumeleGagnantService.java | 290 +++++++++ .../service/JumeleGagnantServiceTest.java | 47 ++ .../com/pmu/jumele/util/CombinaisonUtil.java | 24 + .../com/pmu/jumele/util/FormulesTable.java | 35 ++ .../pmu/mali/apiplr/ApiPlrApplication.java | 13 + src/main/java/com/pmu/mali/model/Pari.java | 16 + .../com/pmu/mali/model/ResultatCourse.java | 16 + .../java/com/pmu/mali/model/TypePari.java | 6 + .../pmu/mali/service/CalculPariService.java | 367 ++++++++++++ .../controller/CourseController.java | 40 ++ .../controller/ParisController.java | 43 ++ .../com/pmumali/jumeleordre/dto/GainsDto.java | 11 + .../com/pmumali/jumeleordre/dto/ParisDto.java | 13 + .../jumeleordre/dto/ResultatCourseDto.java | 14 + .../exception/ChevalInvalideException.java | 12 + .../CourseDejaTermineeException.java | 8 + .../exception/CourseInvalideException.java | 12 + .../exception/GlobalExceptionHandler.java | 54 ++ .../exception/JumeleOrdreException.java | 11 + .../LimiteMiseDepasseeException.java | 22 + .../NombreChevauxInvalideException.java | 16 + .../exception/PaiementInvalideException.java | 12 + .../exception/ParisInvalideException.java | 12 + .../ResultatCourseInvalideException.java | 12 + .../exception/ValidationException.java | 17 + .../com/pmumali/jumeleordre/model/Cheval.java | 17 + .../com/pmumali/jumeleordre/model/Course.java | 23 + .../com/pmumali/jumeleordre/model/Paris.java | 34 ++ .../jumeleordre/model/ResultatCourse.java | 32 + .../repository/ChevalRepository.java | 8 + .../repository/CourseRepository.java | 11 + .../repository/ParisRepository.java | 14 + .../repository/ResultatCourseRepository.java | 9 + .../jumeleordre/service/CourseService.java | 142 +++++ .../jumeleordre/service/ParisService.java | 87 +++ .../service/ResultatCourseService.java | 154 +++++ .../controller/ControleurQuartePlus.java | 97 +++ .../exception/GestionnaireExceptions.java | 24 + .../pmumali/quarteplus/model/Cagnotte.java | 23 + .../quarteplus/model/CalculCombinaison.java | 13 + .../com/pmumali/quarteplus/model/Cheval.java | 22 + .../com/pmumali/quarteplus/model/Course.java | 28 + .../pmumali/quarteplus/model/Paiement.java | 27 + .../quarteplus/model/PariQuartePlus.java | 37 ++ .../com/pmumali/quarteplus/model/Parieur.java | 20 + .../quarteplus/model/ReponsePaiement.java | 13 + .../pmumali/quarteplus/model/RequetePari.java | 13 + .../quarteplus/model/RequeteResultat.java | 17 + .../quarteplus/model/ResultatCourse.java | 42 ++ .../quarteplus/model/StatutCourse.java | 5 + .../quarteplus/model/TypeDeadHeat.java | 12 + .../quarteplus/model/TypePaiement.java | 9 + .../pmumali/quarteplus/model/TypePari.java | 6 + .../repository/CagnotteRepository.java | 10 + .../repository/ChevalRepository.java | 10 + .../repository/CourseRepository.java | 11 + .../repository/PaiementRepository.java | 10 + .../repository/PariQuartePlusRepository.java | 10 + .../repository/ParieurRepository.java | 11 + .../repository/ResultatCourseRepository.java | 8 + .../quarteplus/service/ServiceQuartePlus.java | 563 ++++++++++++++++++ .../quatro/controller/ControllerQuatro.java | 76 +++ .../GestionnaireExceptionsGlobal.java | 24 + .../java/com/pmumali/quatro/model/Cheval.java | 29 + .../java/com/pmumali/quatro/model/Course.java | 35 ++ .../com/pmumali/quatro/model/Paiement.java | 32 + .../java/com/pmumali/quatro/model/Pari.java | 43 ++ .../com/pmumali/quatro/model/Parieur.java | 25 + .../com/pmumali/quatro/model/Position.java | 5 + .../pmumali/quatro/model/ReponsePaiement.java | 13 + .../com/pmumali/quatro/model/RequetePari.java | 13 + .../quatro/model/RequeteResultatCourse.java | 16 + .../pmumali/quatro/model/ResultatCourse.java | 59 ++ .../pmumali/quatro/model/StatutCourse.java | 5 + .../pmumali/quatro/model/TypePaiement.java | 5 + .../com/pmumali/quatro/model/TypePari.java | 6 + .../quatro/repository/ChevalRepository.java | 11 + .../quatro/repository/CourseRepository.java | 11 + .../quatro/repository/PaiementRepository.java | 11 + .../quatro/repository/PariRepository.java | 12 + .../quatro/repository/ParieurRepository.java | 10 + .../repository/ResultatCourseRepository.java | 8 + .../pmumali/quatro/service/ServiceQuatro.java | 283 +++++++++ .../simple/controller/CourseController.java | 28 + .../simple/controller/PariController.java | 68 +++ .../com/pmumali/simple/dto/ChevalDto.java | 23 + .../pmumali/simple/dto/CombinaisonDto.java | 12 + .../com/pmumali/simple/dto/PariRequest.java | 15 + .../com/pmumali/simple/dto/PariResponse.java | 211 +++++++ .../pmumali/simple/dto/ResultatCourseDto.java | 16 + .../simple/exception/PariException.java | 84 +++ .../java/com/pmumali/simple/model/Cheval.java | 68 +++ .../java/com/pmumali/simple/model/Client.java | 287 +++++++++ .../com/pmumali/simple/model/Combinaison.java | 35 ++ .../java/com/pmumali/simple/model/Course.java | 92 +++ .../java/com/pmumali/simple/model/Pari.java | 108 ++++ .../pmumali/simple/model/ResultatCourse.java | 192 ++++++ .../com/pmumali/simple/model/Transaction.java | 33 + .../pmumali/simple/model/enums/TypePari.java | 6 + .../simple/repository/ChevalRepository.java | 7 + .../simple/repository/CourseRepository.java | 7 + .../simple/repository/PariRepository.java | 72 +++ .../simple/service/CalculRapportService.java | 138 +++++ .../simple/service/FormulaireService.java | 48 ++ .../pmumali/simple/service/PariService.java | 309 ++++++++++ .../simple/service/PariServiceTest.java | 43 ++ .../simple/service/ValidationPariService.java | 116 ++++ .../trio/controller/ParisTrioController.java | 25 + .../controller/ResultatCourseController.java | 24 + .../com/pmumali/trio/dto/ParisTrioDto.java | 15 + .../pmumali/trio/dto/ResultatCourseDto.java | 22 + .../exception/ChevalNonPartantException.java | 7 + .../CourseDejaTermineeException.java | 7 + .../exception/CourseInvalideException.java | 14 + .../LimiteMiseDepasseeException.java | 21 + .../NombreChevauxInvalideException.java | 7 + .../exception/ParisInvalideException.java | 14 + .../exception/ParisTrioInvalideException.java | 7 + .../java/com/pmumali/trio/model/Cagnotte.java | 57 ++ .../java/com/pmumali/trio/model/Cheval.java | 17 + .../java/com/pmumali/trio/model/Course.java | 22 + .../com/pmumali/trio/model/ParisTrio.java | 43 ++ .../pmumali/trio/model/ResultatCourse.java | 41 ++ .../trio/repository/CagnotteRepository.java | 29 + .../trio/repository/ChevalRepository.java | 27 + .../trio/repository/CourseRepository.java | 24 + .../trio/repository/ParisTrioRepository.java | 44 ++ .../repository/ResultatCourseRepository.java | 26 + .../trio/service/ParisTrioService.java | 87 +++ .../trio/service/ResultatCourseService.java | 189 ++++++ .../controller/ParisTrioOrdreController.java | 25 + .../controller/ResultatCourseController.java | 24 + .../trioordre/dto/ParisTrioOrdreDto.java | 15 + .../trioordre/dto/ResultatCourseDto.java | 16 + .../CagnotteDejaUtiliseeException.java | 7 + .../exception/CagnotteException.java | 7 + .../CagnotteNonDisponibleException.java | 7 + .../trioordre/exception/ChevalException.java | 7 + .../exception/ChevalNonPartantException.java | 7 + .../exception/ChevalNonTrouveException.java | 7 + .../CombinaisonInvalideException.java | 7 + .../CourseDejaTermineeException.java | 7 + .../trioordre/exception/CourseException.java | 7 + .../exception/CourseNonEligibleException.java | 10 + .../exception/GlobalExceptionHandler.java | 65 ++ .../LimiteMiseDepasseeException.java | 16 + .../exception/MiseInvalideException.java | 16 + .../exception/ParisTrioOrdreException.java | 7 + .../exception/ResultatCourseException.java | 7 + .../ResultatCourseInvalideException.java | 14 + .../ResultatDejaEnregistreException.java | 7 + .../exception/ResultatIncompletException.java | 7 + .../exception/TrioOrdreException.java | 11 + .../com/pmumali/trioordre/model/Cagnotte.java | 31 + .../com/pmumali/trioordre/model/Cheval.java | 17 + .../com/pmumali/trioordre/model/Course.java | 26 + .../trioordre/model/ParisTrioOrdre.java | 51 ++ .../trioordre/model/ResultatCourse.java | 47 ++ .../repository/CagnotteRepository.java | 14 + .../repository/ChevalRepository.java | 54 ++ .../repository/CourseRepository.java | 47 ++ .../repository/ParisTrioOrdreRepository.java | 15 + .../repository/ResultatCourseRepository.java | 7 + .../service/ParisTrioOrdreService.java | 81 +++ .../service/ResultatCourseService.java | 223 +++++++ .../triplet/controller/CourseController.java | 64 ++ .../triplet/controller/PariController.java | 83 +++ .../controller/ResultatController.java | 47 ++ .../com/pmumali/triplet/model/Cagnotte.java | 47 ++ .../com/pmumali/triplet/model/Cheval.java | 39 ++ .../com/pmumali/triplet/model/Course.java | 44 ++ .../java/com/pmumali/triplet/model/Pari.java | 65 ++ .../pmumali/triplet/model/ResultatCourse.java | 46 ++ .../repository/CagnotteRepository.java | 31 + .../triplet/repository/ChevalRepository.java | 33 + .../triplet/repository/CourseRepository.java | 16 + .../triplet/repository/PariRepository.java | 53 ++ .../repository/ResultatCourseRepository.java | 35 ++ .../repository/StatistiqueRepository.java | 84 +++ .../triplet/service/CagnotteService.java | 79 +++ .../triplet/service/CalculGainsService.java | 450 ++++++++++++++ .../triplet/service/CourseService.java | 58 ++ .../pmumali/triplet/service/PariService.java | 75 +++ .../service/ResultatCourseService.java | 27 + .../service/ValidationPariService.java | 79 +++ src/main/resources/application.properties | 1 + .../mali/apiplr/ApiPlrApplicationTests.java | 13 + 201 files changed, 9146 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/pmu/jumele/controller/JumeleController.java create mode 100644 src/main/java/com/pmu/jumele/dto/PaiementResponse.java create mode 100644 src/main/java/com/pmu/jumele/dto/PariRequest.java create mode 100644 src/main/java/com/pmu/jumele/dto/PositionsRequest.java create mode 100644 src/main/java/com/pmu/jumele/entity/PariEntity.java create mode 100644 src/main/java/com/pmu/jumele/repository/PariRepository.java create mode 100644 src/main/java/com/pmu/jumele/service/JumeleGagnantService.java create mode 100644 src/main/java/com/pmu/jumele/service/JumeleGagnantServiceTest.java create mode 100644 src/main/java/com/pmu/jumele/util/CombinaisonUtil.java create mode 100644 src/main/java/com/pmu/jumele/util/FormulesTable.java create mode 100644 src/main/java/com/pmu/mali/apiplr/ApiPlrApplication.java create mode 100644 src/main/java/com/pmu/mali/model/Pari.java create mode 100644 src/main/java/com/pmu/mali/model/ResultatCourse.java create mode 100644 src/main/java/com/pmu/mali/model/TypePari.java create mode 100644 src/main/java/com/pmu/mali/service/CalculPariService.java create mode 100644 src/main/java/com/pmumali/jumeleordre/controller/CourseController.java create mode 100644 src/main/java/com/pmumali/jumeleordre/controller/ParisController.java create mode 100644 src/main/java/com/pmumali/jumeleordre/dto/GainsDto.java create mode 100644 src/main/java/com/pmumali/jumeleordre/dto/ParisDto.java create mode 100644 src/main/java/com/pmumali/jumeleordre/dto/ResultatCourseDto.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/ChevalInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/CourseDejaTermineeException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/CourseInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/JumeleOrdreException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/LimiteMiseDepasseeException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/NombreChevauxInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/PaiementInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/ParisInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/ResultatCourseInvalideException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/exception/ValidationException.java create mode 100644 src/main/java/com/pmumali/jumeleordre/model/Cheval.java create mode 100644 src/main/java/com/pmumali/jumeleordre/model/Course.java create mode 100644 src/main/java/com/pmumali/jumeleordre/model/Paris.java create mode 100644 src/main/java/com/pmumali/jumeleordre/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/jumeleordre/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/jumeleordre/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/jumeleordre/repository/ParisRepository.java create mode 100644 src/main/java/com/pmumali/jumeleordre/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/jumeleordre/service/CourseService.java create mode 100644 src/main/java/com/pmumali/jumeleordre/service/ParisService.java create mode 100644 src/main/java/com/pmumali/jumeleordre/service/ResultatCourseService.java create mode 100644 src/main/java/com/pmumali/quarteplus/controller/ControleurQuartePlus.java create mode 100644 src/main/java/com/pmumali/quarteplus/exception/GestionnaireExceptions.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/Cagnotte.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/CalculCombinaison.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/Cheval.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/Course.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/Paiement.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/PariQuartePlus.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/Parieur.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/ReponsePaiement.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/RequetePari.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/RequeteResultat.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/StatutCourse.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/TypeDeadHeat.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/TypePaiement.java create mode 100644 src/main/java/com/pmumali/quarteplus/model/TypePari.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/CagnotteRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/PaiementRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/PariQuartePlusRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/ParieurRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/quarteplus/service/ServiceQuartePlus.java create mode 100644 src/main/java/com/pmumali/quatro/controller/ControllerQuatro.java create mode 100644 src/main/java/com/pmumali/quatro/exception/GestionnaireExceptionsGlobal.java create mode 100644 src/main/java/com/pmumali/quatro/model/Cheval.java create mode 100644 src/main/java/com/pmumali/quatro/model/Course.java create mode 100644 src/main/java/com/pmumali/quatro/model/Paiement.java create mode 100644 src/main/java/com/pmumali/quatro/model/Pari.java create mode 100644 src/main/java/com/pmumali/quatro/model/Parieur.java create mode 100644 src/main/java/com/pmumali/quatro/model/Position.java create mode 100644 src/main/java/com/pmumali/quatro/model/ReponsePaiement.java create mode 100644 src/main/java/com/pmumali/quatro/model/RequetePari.java create mode 100644 src/main/java/com/pmumali/quatro/model/RequeteResultatCourse.java create mode 100644 src/main/java/com/pmumali/quatro/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/quatro/model/StatutCourse.java create mode 100644 src/main/java/com/pmumali/quatro/model/TypePaiement.java create mode 100644 src/main/java/com/pmumali/quatro/model/TypePari.java create mode 100644 src/main/java/com/pmumali/quatro/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/quatro/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/quatro/repository/PaiementRepository.java create mode 100644 src/main/java/com/pmumali/quatro/repository/PariRepository.java create mode 100644 src/main/java/com/pmumali/quatro/repository/ParieurRepository.java create mode 100644 src/main/java/com/pmumali/quatro/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/quatro/service/ServiceQuatro.java create mode 100644 src/main/java/com/pmumali/simple/controller/CourseController.java create mode 100644 src/main/java/com/pmumali/simple/controller/PariController.java create mode 100644 src/main/java/com/pmumali/simple/dto/ChevalDto.java create mode 100644 src/main/java/com/pmumali/simple/dto/CombinaisonDto.java create mode 100644 src/main/java/com/pmumali/simple/dto/PariRequest.java create mode 100644 src/main/java/com/pmumali/simple/dto/PariResponse.java create mode 100644 src/main/java/com/pmumali/simple/dto/ResultatCourseDto.java create mode 100644 src/main/java/com/pmumali/simple/exception/PariException.java create mode 100644 src/main/java/com/pmumali/simple/model/Cheval.java create mode 100644 src/main/java/com/pmumali/simple/model/Client.java create mode 100644 src/main/java/com/pmumali/simple/model/Combinaison.java create mode 100644 src/main/java/com/pmumali/simple/model/Course.java create mode 100644 src/main/java/com/pmumali/simple/model/Pari.java create mode 100644 src/main/java/com/pmumali/simple/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/simple/model/Transaction.java create mode 100644 src/main/java/com/pmumali/simple/model/enums/TypePari.java create mode 100644 src/main/java/com/pmumali/simple/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/simple/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/simple/repository/PariRepository.java create mode 100644 src/main/java/com/pmumali/simple/service/CalculRapportService.java create mode 100644 src/main/java/com/pmumali/simple/service/FormulaireService.java create mode 100644 src/main/java/com/pmumali/simple/service/PariService.java create mode 100644 src/main/java/com/pmumali/simple/service/PariServiceTest.java create mode 100644 src/main/java/com/pmumali/simple/service/ValidationPariService.java create mode 100644 src/main/java/com/pmumali/trio/controller/ParisTrioController.java create mode 100644 src/main/java/com/pmumali/trio/controller/ResultatCourseController.java create mode 100644 src/main/java/com/pmumali/trio/dto/ParisTrioDto.java create mode 100644 src/main/java/com/pmumali/trio/dto/ResultatCourseDto.java create mode 100644 src/main/java/com/pmumali/trio/exception/ChevalNonPartantException.java create mode 100644 src/main/java/com/pmumali/trio/exception/CourseDejaTermineeException.java create mode 100644 src/main/java/com/pmumali/trio/exception/CourseInvalideException.java create mode 100644 src/main/java/com/pmumali/trio/exception/LimiteMiseDepasseeException.java create mode 100644 src/main/java/com/pmumali/trio/exception/NombreChevauxInvalideException.java create mode 100644 src/main/java/com/pmumali/trio/exception/ParisInvalideException.java create mode 100644 src/main/java/com/pmumali/trio/exception/ParisTrioInvalideException.java create mode 100644 src/main/java/com/pmumali/trio/model/Cagnotte.java create mode 100644 src/main/java/com/pmumali/trio/model/Cheval.java create mode 100644 src/main/java/com/pmumali/trio/model/Course.java create mode 100644 src/main/java/com/pmumali/trio/model/ParisTrio.java create mode 100644 src/main/java/com/pmumali/trio/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/trio/repository/CagnotteRepository.java create mode 100644 src/main/java/com/pmumali/trio/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/trio/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/trio/repository/ParisTrioRepository.java create mode 100644 src/main/java/com/pmumali/trio/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/trio/service/ParisTrioService.java create mode 100644 src/main/java/com/pmumali/trio/service/ResultatCourseService.java create mode 100644 src/main/java/com/pmumali/trioordre/controller/ParisTrioOrdreController.java create mode 100644 src/main/java/com/pmumali/trioordre/controller/ResultatCourseController.java create mode 100644 src/main/java/com/pmumali/trioordre/dto/ParisTrioOrdreDto.java create mode 100644 src/main/java/com/pmumali/trioordre/dto/ResultatCourseDto.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CagnotteDejaUtiliseeException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CagnotteException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CagnotteNonDisponibleException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ChevalException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ChevalNonPartantException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ChevalNonTrouveException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CombinaisonInvalideException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CourseDejaTermineeException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CourseException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/CourseNonEligibleException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/LimiteMiseDepasseeException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/MiseInvalideException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ParisTrioOrdreException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ResultatCourseException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ResultatCourseInvalideException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ResultatDejaEnregistreException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/ResultatIncompletException.java create mode 100644 src/main/java/com/pmumali/trioordre/exception/TrioOrdreException.java create mode 100644 src/main/java/com/pmumali/trioordre/model/Cagnotte.java create mode 100644 src/main/java/com/pmumali/trioordre/model/Cheval.java create mode 100644 src/main/java/com/pmumali/trioordre/model/Course.java create mode 100644 src/main/java/com/pmumali/trioordre/model/ParisTrioOrdre.java create mode 100644 src/main/java/com/pmumali/trioordre/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/trioordre/repository/CagnotteRepository.java create mode 100644 src/main/java/com/pmumali/trioordre/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/trioordre/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/trioordre/repository/ParisTrioOrdreRepository.java create mode 100644 src/main/java/com/pmumali/trioordre/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/trioordre/service/ParisTrioOrdreService.java create mode 100644 src/main/java/com/pmumali/trioordre/service/ResultatCourseService.java create mode 100644 src/main/java/com/pmumali/triplet/controller/CourseController.java create mode 100644 src/main/java/com/pmumali/triplet/controller/PariController.java create mode 100644 src/main/java/com/pmumali/triplet/controller/ResultatController.java create mode 100644 src/main/java/com/pmumali/triplet/model/Cagnotte.java create mode 100644 src/main/java/com/pmumali/triplet/model/Cheval.java create mode 100644 src/main/java/com/pmumali/triplet/model/Course.java create mode 100644 src/main/java/com/pmumali/triplet/model/Pari.java create mode 100644 src/main/java/com/pmumali/triplet/model/ResultatCourse.java create mode 100644 src/main/java/com/pmumali/triplet/repository/CagnotteRepository.java create mode 100644 src/main/java/com/pmumali/triplet/repository/ChevalRepository.java create mode 100644 src/main/java/com/pmumali/triplet/repository/CourseRepository.java create mode 100644 src/main/java/com/pmumali/triplet/repository/PariRepository.java create mode 100644 src/main/java/com/pmumali/triplet/repository/ResultatCourseRepository.java create mode 100644 src/main/java/com/pmumali/triplet/repository/StatistiqueRepository.java create mode 100644 src/main/java/com/pmumali/triplet/service/CagnotteService.java create mode 100644 src/main/java/com/pmumali/triplet/service/CalculGainsService.java create mode 100644 src/main/java/com/pmumali/triplet/service/CourseService.java create mode 100644 src/main/java/com/pmumali/triplet/service/PariService.java create mode 100644 src/main/java/com/pmumali/triplet/service/ResultatCourseService.java create mode 100644 src/main/java/com/pmumali/triplet/service/ValidationPariService.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/pmu/mali/apiplr/ApiPlrApplicationTests.java 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 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + 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() { + } + +}