done all the implementations of each parts yet
This commit is contained in:
317
README.md
Normal file
317
README.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Documentation API — PMU PLR
|
||||
|
||||
> **API de gestion des paris hippiques (PMU)**
|
||||
> Ce projet implémente la logique complète de gestion de **chevaux**, **courses**, **paris**, **résultats** et **gains**.
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Présentation générale](#présentation-générale)
|
||||
2. [Installation & tests](#installation--tests)
|
||||
- [Prérequis](#prérequis)
|
||||
- [Clonage du projet](#clonage-du-projet)
|
||||
- [Compilation & lancement](#compilation--lancement)
|
||||
- [Tests avec Postman](#tests-avec-postman)
|
||||
3. [Conventions API](#conventions-api)
|
||||
4. [Flux typique (Happy Path)](#flux-typique-happy-path)
|
||||
5. [Entités & Endpoints](#entités--endpoints)
|
||||
- [Chevaux](#1-chevaux)
|
||||
- [Courses & Inscriptions](#2-courses--inscriptions)
|
||||
- [Paris](#3-paris)
|
||||
- [Résultats](#4-résultats)
|
||||
- [Gains](#5-gains)
|
||||
6. [Règles métier & validations](#règles-métier--validations)
|
||||
7. [Recettes de test](#recettes-de-test)
|
||||
- [A) Flux complet](#a-flux-complet-end-to-end)
|
||||
- [B) Validation & contraintes](#b-validation--contraintes)
|
||||
- [C) Cas d’ex æquo (Dead-Heat)](#c-cas-dex-æquo-dead-heat)
|
||||
- [D) Gestion NP/DQ](#d-gestion-npdq)
|
||||
- [E) Idempotence du règlement](#e-idempotence-du-règlement)
|
||||
8. [Exemples de payloads](#exemples-de-payloads)
|
||||
|
||||
---
|
||||
|
||||
## Présentation générale
|
||||
|
||||
Cette API gère le cycle complet du **Pari Mutuel Urbain (PMU)** :
|
||||
|
||||
- Gestion des **courses** (création, planification, mise à jour, clôture).
|
||||
- Inscription et gestion des **chevaux** (insertion, participation, non-partant, disqualification).
|
||||
- Création et mise à jour des **paris** (tous types : simple, couple, trio, quarté, quinté, multi, etc.).
|
||||
- Déclaration des **résultats officiels**, avec gestion des ex æquo (Dead-Heat).
|
||||
- Calcul et attribution des **gains** selon les règles PMU.
|
||||
|
||||
---
|
||||
|
||||
## Installation & tests
|
||||
|
||||
### Prérequis
|
||||
|
||||
- **Java 17+**
|
||||
- **Maven**
|
||||
- **PostgreSQL** (base `pmu`)
|
||||
- **Postman** (pour importer la collection fournie)
|
||||
|
||||
### Clonage du projet
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<ton-org>/<ton-repo>.git
|
||||
cd <ton-repo>
|
||||
```
|
||||
|
||||
### Compilation & lancement
|
||||
|
||||
```bash
|
||||
./mvnw clean install
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
Le serveur démarre sur :
|
||||
👉 http://localhost:8080
|
||||
|
||||
### Tests avec Postman
|
||||
|
||||
Importer la collection :
|
||||
1. PMU PLR API - Full Test Collection.postman_collection.json
|
||||
2. Définir la variable d’environnement baseUrl = http://localhost:8080
|
||||
2. Exécuter les requêtes dans l’ordre :
|
||||
- Création course → Ajout chevaux → Paris → Résultat → Règlement → Vérification gains.
|
||||
|
||||
## Conventions API
|
||||
|
||||
- Base URL : {{baseUrl}}
|
||||
|
||||
- Format JSON
|
||||
|
||||
- Enums :
|
||||
- CourseStatue = **PLANIFIEE | EN_COURS | CLOTUREE | ANNULEE**
|
||||
- PariType = **SIMPLE_GAGNANT | SIMPLE_PLACE | COUPLE_GAGNANT | COUPLE_ORDRE | TRIO | TRIO_ORDRE | TIERCE | QUARTE | QUINTE | QUINTE_PLUS | MULTI**
|
||||
|
||||
- Dead-Heat : plusieurs chevaux peuvent partager le même rang.
|
||||
|
||||
## Flux typique (Happy Path)
|
||||
|
||||
1. Créer une course (statut = PLANIFIEE).
|
||||
2. Inscrire les chevaux (simple ou bulk).
|
||||
3. Créer des paris (tant que la course est PLANIFIEE).
|
||||
4. Déclarer les résultats officiels (Dead-Heat permis).
|
||||
5. Fixer le Numéro+ pour Quinté+.
|
||||
6. Régler la course (calcul des gains).
|
||||
7. Vérifier les rapports et gains.
|
||||
|
||||
## Entités & Endpoints
|
||||
### 1. Chevaux
|
||||
|
||||
Endpoints pour la gestion des données des chevaux, indépendamment de leur inscription à une course.
|
||||
|
||||
| Méthode | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `GET` | `/api/chevaux` | Lister tous les chevaux. |
|
||||
| `GET` | `/api/chevaux/{id}` | Obtenir les détails d'un cheval. |
|
||||
| `POST` | `/api/chevaux` | Créer un nouveau cheval. |
|
||||
| `PUT` | `/api/chevaux/{id}` | Modifier les informations d'un cheval existant. |
|
||||
| `DELETE` | `/api/chevaux/{id}` | Supprimer un cheval de la base de données. |
|
||||
|
||||
---
|
||||
|
||||
### 2. Courses & Inscriptions
|
||||
|
||||
Gérer la création et la modification des courses et des inscriptions de chevaux.
|
||||
|
||||
| Méthode | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `GET` | `/api/courses` | Lister toutes les courses. |
|
||||
| `GET` | `/api/courses/{id}` | Obtenir les détails d'une course spécifique. |
|
||||
| `GET` | `/api/courses/{STATUS: CourseStatue}` | Obtenir les courses spécifique par status (CourseStatue). |
|
||||
| `POST` | `/api/courses` | Créer une nouvelle course. |
|
||||
| `PUT` | `/api/courses/{id}` | Modifier une course existante. |
|
||||
| `DELETE` | `/api/courses/{id}` | Supprimer une course. |
|
||||
| `POST` | `/api/courses/chevaux` | Ajouter un seul cheval à une course. |
|
||||
| `POST` | `/api/courses/chevaux` | Ajouter un cheval à une course en une seule requête. |
|
||||
| `POST` | `/api/courses/chevaux/bulk` | Ajouter plusieurs chevaux à une course en une seule requête. |
|
||||
| `GET` | `/api/courses/{id}/chevaux` | Lister les chevaux inscrites à une course. |
|
||||
| `PUT` | `/api/courses/cheval-course/{id}/scratch?value={true\|false}` | (Dé)marquer un cheval comme non-partant (NP). |
|
||||
| `PUT` | `/api/courses/cheval-course/{id}/disqualify?value={true\|false}` | (Dé)qualifier un cheval d'une course. |
|
||||
| `DELETE` | `/api/courses/cheval-course/{id}` | Supprimer un cheval d'une course quelqu'en soit la course. |
|
||||
| `DELETE` | `/api/courses/cheval-course/{id}` | Supprimer un cheval d'une course quelqu'en soit la course. |
|
||||
|
||||
---
|
||||
|
||||
### 3. Paris
|
||||
|
||||
Points d'accès pour la création, la modification et la gestion des paris.
|
||||
|
||||
| Méthode | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `POST` | `/api/paris` | Créer un nouveau pari. |
|
||||
| `GET` | `/api/paris/{id}` | Obtenir le détail d’un pari spécifique. |
|
||||
| `PUT` | `/api/paris/{id}` | Modifier un pari existant. |
|
||||
| `DELETE` | `/api/paris/{id}` | Supprimer un pari. |
|
||||
| `GET` | `/api/paris` | Lister tous les paris (avec filtres possibles). |
|
||||
| `GET` | `/api/paris/course/{courseId}` | Obtenir la liste de tous les paris pour une course spécifique. |
|
||||
| `POST` | `/api/paris/course/settle/{courseId}` | Régler tous les paris d'une course après officialisation des résultats. |
|
||||
| `GET` | `/api/paris/course/report/{courseId}` | Générer un rapport détaillé des paris d'une course. |
|
||||
| `GET` | `/api/gains/pari/{pariId}` | Vérifier le gain associé à un pari donné. |
|
||||
| `GET` | `/api/gains/course/{courseId}` | Lister tous les gains générés pour une course après règlement. |
|
||||
|
||||
---
|
||||
|
||||
### 4. Résultats
|
||||
|
||||
Gérer les résultats officiels et le classement d'une course.
|
||||
|
||||
| Méthode | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `POST` | `/api/resultats` | Créer un résultat pour une course. |
|
||||
| `GET` | `/api/resultats` | Lister tous les résultats. |
|
||||
| `GET` | `/api/resultats/course/{courseId}` | Récupérer le résultat d’une course. |
|
||||
| `PUT` | `/api/resultats/course/{courseId}` | Mettre à jour le résultat d’une course. |
|
||||
| `DELETE` | `/api/resultats/course/{courseId}` | Supprimer le résultat d’une course. |
|
||||
| `GET` | `/api/resultats/course/{courseId}/chevaux` | Lister les chevaux avec leurs rangs. |
|
||||
| `POST` | `/api/resultats/course/{courseId}/chevaux` | Ajouter un cheval dans le résultat. |
|
||||
| `PUT` | `/api/resultats/course/{courseId}/chevaux/{chevalId}` | Mettre à jour ou insérer le rang d’un cheval. |
|
||||
| `DELETE` | `/api/resultats/course/{courseId}/chevaux/{chevalId}` | Supprimer un cheval du résultat. |
|
||||
| `PATCH` | `/api/resultats/course/{courseId}/numero-plus` | Fixer le **Numéro+ gagnant** (Quinté+). |
|
||||
| `PUT` | `/api/resultats/course/{courseId}/numero-plus/draw` | Tirer au sort automatiquement le Numéro+. |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 5. Gains
|
||||
|
||||
Points d'accès pour la vérification des gains et la génération de rapports.
|
||||
| Méthode | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `GET` | `/api/gains/course/{courseId}` | Obtenir les gains de tous les paris d’une course. |
|
||||
| `GET` | `/api/gains/pari/{pariId}` | Obtenir les détails des gains pour un pari donné. |
|
||||
| `POST` | `/api/paris/course/settle/{courseId}` | Régler et distribuer les gains d’une course. |
|
||||
|
||||
|
||||
|
||||
## Règles métier & validations
|
||||
|
||||
Les règles métier et les validations appliquées aux différentes étapes du cycle de vie d'une course, des paris aux résultats.
|
||||
|
||||
---
|
||||
|
||||
### 1. Gestion des courses
|
||||
|
||||
* **Course PLANIFIÉE :** Il s'agit de la seule phase pendant laquelle il est possible d'enregistrer des paris. Toute tentative de créer un pari en dehors de ce statut sera rejetée.
|
||||
* **Résultats créés :** Une fois que les résultats officiels sont enregistrés, le statut de la course est automatiquement mis à jour à **CLÔTURÉE**.
|
||||
|
||||
---
|
||||
|
||||
### 2. Validations des paris
|
||||
|
||||
Les règles suivantes sont appliquées pour garantir la validité des paris :
|
||||
|
||||
* **Positions obligatoires :** Pour les paris ordonnés, toutes les positions doivent être renseignées.
|
||||
* **Doublons interdits :** Un ticket de pari ne peut pas contenir de doublons de cheval ou de position.
|
||||
* **Dead-Heat :** Le système autorise les ex æquo (chevaux arrivant à égalité) dans les résultats.
|
||||
* **Quinté+ :** Chaque ticket de Quinté+ se voit attribuer un **Numéro+** aléatoire au moment de la création du pari.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Recettes de test
|
||||
|
||||
Ce document détaille les différentes recettes de test pour valider le bon fonctionnement de l'API.
|
||||
|
||||
---
|
||||
|
||||
### A) Flux complet (end-to-end)
|
||||
|
||||
1. Créer une course.
|
||||
2. Inscrire des chevaux à la course.
|
||||
3. Placer des paris sur la course.
|
||||
4. Déclarer les résultats officiels.
|
||||
5. Régler la course (calcul des gains).
|
||||
6. Vérifier les gains générés pour les paris gagnants.
|
||||
|
||||
---
|
||||
|
||||
### B) Validation & contraintes
|
||||
|
||||
* **Pari avec cheval en double :** Tenter de placer un pari contenant deux fois le même cheval. Le système doit retourner une erreur **400 (Bad Request)**.
|
||||
* **Pari avec cheval NP :** Tenter de placer un pari incluant un cheval marqué comme non-partant (NP). Le système doit rejeter le pari.
|
||||
|
||||
---
|
||||
|
||||
### C) Cas d’ex æquo (Dead-Heat)
|
||||
|
||||
* Déclarer deux chevaux ayant le même rang (par exemple, `rang=1`).
|
||||
* Vérifier que le calcul du pari de type **SIMPLE_GAGNANT** fonctionne correctement pour l'un ou l'autre des chevaux à égalité.
|
||||
|
||||
---
|
||||
|
||||
### D) Gestion NP/DQ
|
||||
|
||||
* **Réintégration (NP) :** Envoyer une requête `PUT` avec `scratch?value=false` pour réintégrer un cheval précédemment non-partant.
|
||||
* **Requalification (DQ) :** Envoyer une requête `PUT` avec `disqualify?value=false` pour requalifier un cheval précédemment disqualifié.
|
||||
|
||||
---
|
||||
|
||||
### E) Idempotence du règlement
|
||||
|
||||
* Tenter de régler une même course à deux reprises (`POST /api/paris/course/settle/{courseId}`). Les résultats du calcul des gains doivent être **identiques** à chaque exécution.
|
||||
|
||||
---
|
||||
|
||||
## Exemples de payloads
|
||||
### 1. Données du pari
|
||||
|
||||
* **Endpoint :** `POST /api/paris`
|
||||
* **Description :** Création d'un pari TRIO.
|
||||
* **Corps de la requête :**
|
||||
```json
|
||||
{
|
||||
"courseId": 4,
|
||||
"pariType": "TRIO",
|
||||
"mise": 3000.00,
|
||||
"bettorRef": "punter-C",
|
||||
"selections": [
|
||||
{ "chevalId": 7, "position": 1 },
|
||||
{ "chevalId": 9, "position": 2 },
|
||||
{ "chevalId": 10, "position": 3 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Données des résultats
|
||||
|
||||
* **Endpoint :** `POST /api/resultats`
|
||||
* **Description :** Enregistrement des résultats officiels de la course.
|
||||
* **Corps de la requête :**
|
||||
```json
|
||||
[
|
||||
{ "chevalId": 9, "rang": 1 },
|
||||
{ "chevalId": 7, "rang": 2 },
|
||||
{ "chevalId": 10, "rang": 3 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Numéro+ (non pertinent pour ce pari)
|
||||
|
||||
* **Endpoint :** `PATCH /api/resultats/course/{courseId}/numero-plus`
|
||||
* **Description :** Enregistrement du Numéro+ pour la course.
|
||||
* **Corps de la requête :**
|
||||
```json
|
||||
{ "numeroPlusGagnant": "654321" }
|
||||
```
|
||||
* **Note :** Le Numéro+ n'a pas d'impact sur le pari de type TRIO, mais est inclus pour documenter le flux complet de règlement d'une course.
|
||||
|
||||
---
|
||||
|
||||
### 4. Résultat attendu
|
||||
|
||||
Le pari est **gagnant** si les trois chevaux sélectionnés figurent parmi les trois premiers de l'arrivée, quel que soit l'ordre. Dans ce cas, les chevaux `7`, `9` et `10` sont bien dans les trois premières positions du classement officiel. Le pari doit être réglé comme un gain.
|
||||
|
||||
------
|
||||
|
||||
Bon testing ): !!!
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.pmumali.plr.config;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalApiExceptionHandler {
|
||||
|
||||
// ---------- Helpers ----------
|
||||
private Map<String, Object> body(HttpStatus status, String message, WebRequest req) {
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
map.put("timestamp", OffsetDateTime.now().toString());
|
||||
map.put("status", status.value());
|
||||
map.put("error", status.getReasonPhrase());
|
||||
map.put("message", message != null ? message : "");
|
||||
map.put("path", extractPath(req));
|
||||
return map;
|
||||
}
|
||||
|
||||
private String extractPath(WebRequest req) {
|
||||
if (req instanceof ServletWebRequest swr && swr.getRequest() != null) {
|
||||
return swr.getRequest().getRequestURI();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ---------- 404 ----------
|
||||
@ExceptionHandler(EntityNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public Map<String, Object> notFound(EntityNotFoundException e, WebRequest req) {
|
||||
return body(HttpStatus.NOT_FOUND, e.getMessage(), req);
|
||||
}
|
||||
|
||||
// ---------- 400: @Valid on @RequestBody (field errors) ----------
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Map<String, Object> badRequestValidation(MethodArgumentNotValidException e, WebRequest req) {
|
||||
var map = body(HttpStatus.BAD_REQUEST, "Validation error", req);
|
||||
List<Map<String,Object>> fieldErrors = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(fe -> {
|
||||
Map<String,Object> m = new LinkedHashMap<>();
|
||||
m.put("field", fe.getField());
|
||||
m.put("message", fe.getDefaultMessage());
|
||||
m.put("rejectedValue", fe.getRejectedValue());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
map.put("errors", fieldErrors);
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---------- 400: @Validated on params/path/query (ConstraintViolation) ----------
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Map<String, Object> badRequestConstraint(ConstraintViolationException e, WebRequest req) {
|
||||
var map = body(HttpStatus.BAD_REQUEST, "Constraint violation", req);
|
||||
List<Map<String, Object>> violations = e.getConstraintViolations().stream()
|
||||
.map(v -> violationEntry(v)).toList();
|
||||
map.put("errors", violations);
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> violationEntry(ConstraintViolation<?> v) {
|
||||
var m = new LinkedHashMap<String, Object>();
|
||||
m.put("property", v.getPropertyPath() != null ? v.getPropertyPath().toString() : "");
|
||||
m.put("message", v.getMessage());
|
||||
m.put("invalidValue", v.getInvalidValue());
|
||||
return m;
|
||||
}
|
||||
|
||||
// ---------- 400: bad JSON / wrong types / missing params ----------
|
||||
@ExceptionHandler({
|
||||
HttpMessageNotReadableException.class,
|
||||
MissingServletRequestParameterException.class,
|
||||
MethodArgumentTypeMismatchException.class
|
||||
})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Map<String, Object> badRequestParse(Exception e, WebRequest req) {
|
||||
return body(HttpStatus.BAD_REQUEST, e.getMessage(), req);
|
||||
}
|
||||
|
||||
// ---------- 409: DB constraints ----------
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
@ResponseStatus(HttpStatus.CONFLICT)
|
||||
public Map<String, Object> conflict(DataIntegrityViolationException e, WebRequest req) {
|
||||
Throwable most = e.getMostSpecificCause(); // <- we use the result
|
||||
String msg = (most != null && most.getMessage() != null) ? most.getMessage()
|
||||
: (e.getMessage() != null ? e.getMessage() : "Data integrity violation");
|
||||
return body(HttpStatus.CONFLICT, msg, req);
|
||||
}
|
||||
|
||||
// ---------- 405 / 415 ----------
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
public Map<String, Object> methodNotAllowed(HttpRequestMethodNotSupportedException e, WebRequest req) {
|
||||
return body(HttpStatus.METHOD_NOT_ALLOWED, e.getMessage(), req);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
public Map<String, Object> mediaType(HttpMediaTypeNotSupportedException e, WebRequest req) {
|
||||
return body(HttpStatus.UNSUPPORTED_MEDIA_TYPE, e.getMessage(), req);
|
||||
}
|
||||
|
||||
// ---------- 400: generic illegal argument ----------
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Map<String, Object> badRequest(IllegalArgumentException e, WebRequest req) {
|
||||
return body(HttpStatus.BAD_REQUEST, e.getMessage(), req);
|
||||
}
|
||||
|
||||
// ---------- 500: catch-all ----------
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Map<String, Object> unhandled(Exception e, WebRequest req) {
|
||||
// Avoid leaking stack traces; log them instead if you have a logger here.
|
||||
String msg = (e.getMessage() != null) ? e.getMessage() : e.getClass().getSimpleName();
|
||||
return body(HttpStatus.INTERNAL_SERVER_ERROR, msg, req);
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,12 @@ public class ChevalController {
|
||||
public ResponseEntity<ChevalDto> create(@RequestBody ChevalDto dto) {
|
||||
Cheval ch = new Cheval();
|
||||
ch.setNom(dto.nom());
|
||||
ch.setNumero(dto.numero());
|
||||
ch.setNomEcurie(dto.nomEcurie());
|
||||
ch.setBirthYear(dto.birthYear());
|
||||
|
||||
ch = chevalService.create(ch);
|
||||
|
||||
ChevalDto response = new ChevalDto(ch.getId(), ch.getNom(), ch.getNumero(), ch.getNomEcurie(),
|
||||
ChevalDto response = new ChevalDto(ch.getId(), ch.getNom(), ch.getNomEcurie(),
|
||||
ch.getBirthYear());
|
||||
return ResponseEntity.created(URI.create("/api/chevaux/" + ch.getId())).body(response);
|
||||
}
|
||||
@@ -46,7 +45,7 @@ public class ChevalController {
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ChevalDto>> all() {
|
||||
List<ChevalDto> list = chevalService.all().stream()
|
||||
.map(h -> new ChevalDto(h.getId(), h.getNom(), h.getNumero(), h.getNomEcurie(), h.getBirthYear()))
|
||||
.map(h -> new ChevalDto(h.getId(), h.getNom(), h.getNomEcurie(), h.getBirthYear()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
@@ -55,7 +54,7 @@ public class ChevalController {
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ChevalDto> one(@PathVariable Long id) {
|
||||
Cheval h = chevalService.get(id);
|
||||
ChevalDto dto = new ChevalDto(h.getId(), h.getNom(), h.getNumero(), h.getNomEcurie(), h.getBirthYear());
|
||||
ChevalDto dto = new ChevalDto(h.getId(), h.getNom(), h.getNomEcurie(), h.getBirthYear());
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@@ -64,12 +63,11 @@ public class ChevalController {
|
||||
public ResponseEntity<ChevalDto> update(@PathVariable Long id, @RequestBody ChevalDto dto) {
|
||||
Cheval ch = new Cheval();
|
||||
ch.setNom(dto.nom());
|
||||
ch.setNumero(dto.numero());
|
||||
ch.setNomEcurie(dto.nomEcurie());
|
||||
ch.setBirthYear(dto.birthYear());
|
||||
|
||||
ch = chevalService.update(id, ch);
|
||||
ChevalDto response = new ChevalDto(ch.getId(), ch.getNom(), ch.getNumero(), ch.getNomEcurie(),
|
||||
ChevalDto response = new ChevalDto(ch.getId(), ch.getNom(), ch.getNomEcurie(),
|
||||
ch.getBirthYear());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@@ -44,10 +44,12 @@ public class CourseController {
|
||||
c.setNom(dto.nom());
|
||||
c.setLieu(dto.lieu());
|
||||
c.setDepartureDateTime(dto.departureDateTime());
|
||||
c.setDistance(dto.distance());
|
||||
c.setStatus(dto.status());
|
||||
|
||||
c = courseService.create(c);
|
||||
CourseDto result = new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getStatus());
|
||||
CourseDto result = new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getDistance(),
|
||||
c.getStatus());
|
||||
|
||||
return ResponseEntity.created(URI.create("/api/courses/" + c.getId())).body(result);
|
||||
}
|
||||
@@ -59,7 +61,7 @@ public class CourseController {
|
||||
@PathVariable Long id) {
|
||||
Course course = courseService.get(id);
|
||||
CourseDto response = new CourseDto(course.getId(), course.getNom(), course.getLieu(),
|
||||
course.getDepartureDateTime(), course.getStatus());
|
||||
course.getDepartureDateTime(), course.getDistance(), course.getStatus());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@@ -72,7 +74,8 @@ public class CourseController {
|
||||
: courseService.getCoursesByStatus(status);
|
||||
|
||||
List<CourseDto> dtos = courses.stream()
|
||||
.map(c -> new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getStatus()))
|
||||
.map(c -> new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getDistance(),
|
||||
c.getStatus()))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
@@ -87,12 +90,13 @@ public class CourseController {
|
||||
update.setNom(dto.nom());
|
||||
update.setLieu(dto.lieu());
|
||||
update.setDepartureDateTime(dto.departureDateTime());
|
||||
update.setDistance(dto.distance());
|
||||
update.setStatus(dto.status());
|
||||
|
||||
Course updated = courseService.updateCourse(id, update);
|
||||
|
||||
CourseDto result = new CourseDto(updated.getId(), updated.getNom(), updated.getLieu(),
|
||||
updated.getDepartureDateTime(), updated.getStatus());
|
||||
updated.getDepartureDateTime(), updated.getDistance(), updated.getStatus());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@@ -103,20 +107,11 @@ public class CourseController {
|
||||
*/
|
||||
@Operation(summary = "Supprimer une course")
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteCourse(@PathVariable Long id,
|
||||
@RequestParam(value = "hard", required = false, defaultValue = "false") boolean hard) {
|
||||
public ResponseEntity<?> deleteCourse(@PathVariable Long id) {
|
||||
try {
|
||||
courseService.deleteCourse(id, hard);
|
||||
if (!hard) {
|
||||
// return the cancelled course representation
|
||||
Course c = courseService.get(id);
|
||||
CourseDto dto = new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(),
|
||||
c.getStatus());
|
||||
return ResponseEntity.ok(dto);
|
||||
} else {
|
||||
// hard delete succeeded: no content
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
courseService.deleteCourse(id);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (DataIntegrityViolationException ex) {
|
||||
return ResponseEntity.status(409).body(java.util.Collections.singletonMap("error", ex.getMessage()));
|
||||
}
|
||||
@@ -124,9 +119,9 @@ public class CourseController {
|
||||
|
||||
// Ajout d'un cheval à la course -> POST /api/courses/{id}/chevaux
|
||||
@Operation(summary = "Ajouter un cheval à une course")
|
||||
@PostMapping("/{id}/chevaux")
|
||||
public ResponseEntity<ChevalCourseDto> ajouterCheval(@PathVariable Long id, @RequestBody ChevalCourseDto dto) {
|
||||
ChevalCourse rh = courseService.ajouterCheval(id, dto.chevalId(), dto.numeroCheval());
|
||||
@PostMapping("/chevaux")
|
||||
public ResponseEntity<ChevalCourseDto> ajouterCheval(@RequestBody ChevalCourseDto dto) {
|
||||
ChevalCourse rh = courseService.ajouterCheval(dto.courseId(), dto.chevalId(), dto.numeroCheval());
|
||||
|
||||
ChevalCourseDto response = new ChevalCourseDto(
|
||||
rh.getId(),
|
||||
@@ -136,21 +131,22 @@ public class CourseController {
|
||||
rh.getNonPartant(),
|
||||
rh.getEstDisqualifie());
|
||||
|
||||
return ResponseEntity.created(URI.create("/api/courses/" + id + "/chevaux/" + rh.getId())).body(response);
|
||||
return ResponseEntity.created(URI.create("/api/courses/chevaux/" + rh.getId()))
|
||||
.body(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "Ajouter des chevaux à une course")
|
||||
@PostMapping("/{id}/chevaux/bulk")
|
||||
public ResponseEntity<?> bulkAddChevaux(@PathVariable("id") Long id, @RequestBody BulkChevalCourseRequest request) {
|
||||
@PostMapping("/chevaux/bulk")
|
||||
public ResponseEntity<?> bulkAddChevaux(@RequestBody BulkChevalCourseRequest request) {
|
||||
try {
|
||||
List<ChevalCourse> saved = courseService.ajouterChevauxBulk(id, request);
|
||||
List<ChevalCourse> saved = courseService.ajouterChevauxBulk(request.courseId(), request);
|
||||
|
||||
List<ChevalCourseDto> dtos = saved.stream()
|
||||
.map(rh -> new ChevalCourseDto(rh.getId(), rh.getCourse().getId(), rh.getCheval().getId(),
|
||||
rh.getNumeroCheval(), rh.getNonPartant(), rh.getEstDisqualifie()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.created(URI.create("/api/courses/" + id + "/chevaux")).body(dtos);
|
||||
return ResponseEntity.created(URI.create("/api/courses/chevaux")).body(dtos);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// doublons dans la requête
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
@@ -171,7 +167,7 @@ public class CourseController {
|
||||
}
|
||||
|
||||
// Marquer un cheval comme non-partant (scratch)
|
||||
@Operation(summary = "Marquer un cheval comme non-partant (NP) à une course")
|
||||
@Operation(summary = "Marquer / Dé-marquer un cheval comme non-partant (NP) à une course")
|
||||
@PutMapping("/cheval-course/{chevalCourseId}/scratch")
|
||||
public ResponseEntity<Void> scratch(@PathVariable Long chevalCourseId, @RequestParam boolean value) {
|
||||
courseService.estNonPartant(chevalCourseId, value);
|
||||
@@ -213,7 +209,8 @@ public class CourseController {
|
||||
List<Course> courses = courseService.getCoursesByCourseStatue(status);
|
||||
|
||||
List<CourseDto> dtos = courses.stream()
|
||||
.map(c -> new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getStatus()))
|
||||
.map(c -> new CourseDto(c.getId(), c.getNom(), c.getLieu(), c.getDepartureDateTime(), c.getDistance(),
|
||||
c.getStatus()))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// com.pmumali.plr.controllers.WinnerController.java
|
||||
package com.pmumali.plr.controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pmumali.plr.dtos.GainViewDto;
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.services.GainService;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/gains")
|
||||
@RequiredArgsConstructor
|
||||
public class GainController {
|
||||
|
||||
private final GainService winnerService;
|
||||
|
||||
@Operation(summary = "Lister tous les gagnants d'une course (officiels si disponibles, sinon provisoires)")
|
||||
@GetMapping("/course/{courseId}")
|
||||
public ResponseEntity<?> listWinners(@PathVariable Long courseId) {
|
||||
try {
|
||||
List<GainViewDto> winners = winnerService.listWinners(courseId);
|
||||
return ResponseEntity.ok(winners);
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(404).body(java.util.Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Lister les gagnants d'une course pour un type de pari")
|
||||
@GetMapping("/course/{courseId}/type/{pariType}")
|
||||
public ResponseEntity<?> listWinnersByType(@PathVariable Long courseId, @PathVariable PariType pariType) {
|
||||
try {
|
||||
List<GainViewDto> winners = winnerService.listWinnersByType(courseId, pariType);
|
||||
return ResponseEntity.ok(winners);
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(404).body(java.util.Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Savoir si un pari est gagnant (officiel si disponible, sinon provisoire)")
|
||||
@GetMapping("/pari/{pariId}")
|
||||
public ResponseEntity<?> isWinner(@PathVariable Long pariId) {
|
||||
try {
|
||||
GainViewDto view = winnerService.isWinner(pariId);
|
||||
return ResponseEntity.ok(view);
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(404).body(java.util.Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
package com.pmumali.plr.controllers;
|
||||
|
||||
import java.net.URI;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pmumali.plr.dtos.PariDto;
|
||||
import com.pmumali.plr.dtos.CreatePariRequestDto;
|
||||
import com.pmumali.plr.dtos.PariResponseDto;
|
||||
import com.pmumali.plr.dtos.SettlementSummaryDto;
|
||||
import com.pmumali.plr.dtos.UpdatePariRequestDto;
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.models.Pari;
|
||||
import com.pmumali.plr.services.PariService;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -29,61 +35,72 @@ public class PariController {
|
||||
|
||||
@Operation(summary = "Placer un pari")
|
||||
@PostMapping
|
||||
public ResponseEntity<PariDto> create(@RequestBody PariDto dto) {
|
||||
Pari p = pariService.create(dto.courseId(), dto.chevalId(), dto.pariType(), dto.mise(), dto.bettorRef());
|
||||
PariDto response = new PariDto(p.getId(), p.getCourse().getId(), p.getCheval().getId(), p.getPariType(),
|
||||
p.getMise(), p.getBettorRef());
|
||||
return ResponseEntity.created(URI.create("/api/paris/" + p.getId())).body(response);
|
||||
public ResponseEntity<PariResponseDto> createPari(@Valid @RequestBody CreatePariRequestDto requestDto) {
|
||||
PariResponseDto createdPari = pariService.createPari(requestDto);
|
||||
return new ResponseEntity<>(createdPari, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@Operation(summary = "Mettre à jour un pari (seulement si la course est PLANIFIEE)")
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<PariResponseDto> updatePari(@PathVariable Long id,
|
||||
@Valid @RequestBody UpdatePariRequestDto dto) {
|
||||
return ResponseEntity.ok(pariService.updatePari(id, dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "Supprimer un pari (seulement si la course est PLANIFIEE)")
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deletePari(@PathVariable Long id) {
|
||||
pariService.deletePari(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Lister tous les paris")
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PariDto>> all() {
|
||||
List<PariDto> list = pariService.all().stream()
|
||||
.map(h -> new PariDto(h.getId(), h.getCourse().getId(), h.getCheval().getId(), h.getPariType(),
|
||||
h.getMise(), h.getBettorRef()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(list);
|
||||
public ResponseEntity<List<PariResponseDto>> all() {
|
||||
return ResponseEntity.ok(pariService.all());
|
||||
}
|
||||
|
||||
@Operation(summary = "Récupérer un pari par id")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PariDto> one(@PathVariable Long id) {
|
||||
Pari p = pariService.get(id);
|
||||
PariDto dto = new PariDto(p.getId(), p.getCourse().getId(), p.getCheval().getId(), p.getPariType(), p.getMise(),
|
||||
p.getBettorRef());
|
||||
return ResponseEntity.ok(dto);
|
||||
public ResponseEntity<PariResponseDto> getPariById(@PathVariable Long id) {
|
||||
PariResponseDto pari = pariService.getPariById(id);
|
||||
return ResponseEntity.ok(pari);
|
||||
}
|
||||
|
||||
// Recherche par type (ex: SIMPLE_GAGNANT)
|
||||
@Operation(summary = "Lister les paris par type")
|
||||
@GetMapping("/type/{pariType}")
|
||||
public ResponseEntity<List<PariDto>> getAllByPariType(@PathVariable PariType pariType) {
|
||||
List<PariDto> list = pariService.getParisByPariType(pariType).stream()
|
||||
.map(h -> new PariDto(h.getId(), h.getCourse().getId(), h.getCheval().getId(), h.getPariType(),
|
||||
h.getMise(), h.getBettorRef()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(list);
|
||||
public ResponseEntity<List<PariResponseDto>> getAllByPariType(@PathVariable PariType pariType) {
|
||||
List<PariResponseDto> list = pariService.getParisByPariType(pariType);
|
||||
return new ResponseEntity<>(list, HttpStatus.OK);
|
||||
}
|
||||
|
||||
// Recherche par course et type
|
||||
@Operation(summary = "Lister les paris par course + type")
|
||||
@GetMapping("/course/{courseId}/type/{pariType}")
|
||||
public ResponseEntity<List<PariDto>> getByCourseAndType(@PathVariable Long courseId,
|
||||
public ResponseEntity<List<PariResponseDto>> getByCourseAndType(@PathVariable Long courseId,
|
||||
@PathVariable PariType pariType) {
|
||||
List<PariDto> list = pariService.getParisByCourseAndType(courseId, pariType).stream()
|
||||
.map(h -> new PariDto(h.getId(), h.getCourse().getId(), h.getCheval().getId(), h.getPariType(),
|
||||
h.getMise(), h.getBettorRef()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(list);
|
||||
List<PariResponseDto> list = pariService.getParisByCourseAndType(courseId, pariType);
|
||||
return new ResponseEntity<>(list, HttpStatus.OK);
|
||||
}
|
||||
|
||||
// Somme des mises d'une course / type (utile pour calculs de pools)
|
||||
@Operation(summary = "Total des mises pour une course et un type de pari")
|
||||
@GetMapping("/course/{courseId}/type/{pariType}/sum")
|
||||
public ResponseEntity<java.math.BigDecimal> sumMises(@PathVariable Long courseId, @PathVariable PariType pariType) {
|
||||
java.math.BigDecimal total = pariService.sumMiseByCourseIdAndPariType(courseId, pariType);
|
||||
return ResponseEntity.ok(total);
|
||||
public ResponseEntity<BigDecimal> sumMises(@PathVariable Long courseId, @PathVariable PariType pariType) {
|
||||
return ResponseEntity.ok(pariService.sumMiseByCourseIdAndPariType(courseId, pariType));
|
||||
}
|
||||
|
||||
// ---------- Règlement & rapports ----------
|
||||
@Operation(summary = "Régler tous les paris d'une course (calcul cagnotte, prélèvements, gains)")
|
||||
@PostMapping("/course/settle/{courseId}")
|
||||
public ResponseEntity<SettlementSummaryDto> settleCourse(@PathVariable Long courseId) {
|
||||
return ResponseEntity.ok(pariService.settleCourse(courseId));
|
||||
}
|
||||
|
||||
@Operation(summary = "Rapport de paiement d'une course (par type)")
|
||||
@GetMapping("/course/report/{courseId}")
|
||||
public ResponseEntity<SettlementSummaryDto> getSettlementReport(@PathVariable Long courseId) {
|
||||
return ResponseEntity.ok(pariService.getSettlementReport(courseId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.pmumali.plr.controllers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pmumali.plr.dtos.ResultatChevalDto;
|
||||
import com.pmumali.plr.dtos.ResultatChevalView;
|
||||
import com.pmumali.plr.dtos.ResultatCreationDto;
|
||||
import com.pmumali.plr.dtos.ResultatDto;
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
import com.pmumali.plr.models.ResultatCheval;
|
||||
import com.pmumali.plr.services.ResultatService;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/resultats")
|
||||
@AllArgsConstructor
|
||||
public class ResultatController {
|
||||
|
||||
private final ResultatService resultatService;
|
||||
|
||||
// -------- Resultat CRUD --------
|
||||
|
||||
@Operation(summary = "Créer un résultat pour une course")
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createResultat(@RequestBody ResultatCreationDto dto) {
|
||||
try {
|
||||
var r = resultatService.createResultat(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(r);
|
||||
} catch (IllegalArgumentException | EntityNotFoundException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Lister tous les résultats")
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ResultatDto>> getAllResults() {
|
||||
return ResponseEntity.ok(resultatService.findAllResults());
|
||||
}
|
||||
|
||||
@Operation(summary = "Récupérer un résultat par course id")
|
||||
@GetMapping("/course/{courseId}")
|
||||
public ResponseEntity<?> getByCourse(@PathVariable Long courseId) {
|
||||
try {
|
||||
return ResponseEntity.ok(resultatService.findResultatByCourseId(courseId));
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Mettre à jour le classement complet (reclassement)")
|
||||
@PutMapping("/course/{courseId}")
|
||||
public ResponseEntity<?> update(@PathVariable Long courseId, @RequestBody List<ResultatChevalDto> updated) {
|
||||
try {
|
||||
return ResponseEntity.ok(resultatService.updateResultat(courseId, updated));
|
||||
} catch (IllegalArgumentException | EntityNotFoundException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Supprimer le résultat d'une course")
|
||||
@DeleteMapping("/course/{courseId}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long courseId) {
|
||||
try {
|
||||
resultatService.deleteResultat(courseId);
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Lignes (ResultatCheval) --------
|
||||
@Operation(summary = "Lister les lignes de résultat (par course)")
|
||||
@GetMapping("/course/{courseId}/chevaux")
|
||||
public ResponseEntity<List<ResultatChevalView>> listChevauxOfResultExt(@PathVariable Long courseId) {
|
||||
return ResponseEntity.ok(resultatService.listResultatChevauxViewExt(courseId));
|
||||
}
|
||||
|
||||
@Operation(summary = "Ajouter une ligne au résultat")
|
||||
@PostMapping("/course/{courseId}/chevaux")
|
||||
public ResponseEntity<?> addLine(@PathVariable Long courseId, @RequestBody ResultatChevalDto dto) {
|
||||
try {
|
||||
ResultatChevalView view = resultatService.addResultatCheval(courseId, dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
||||
} catch (IllegalArgumentException | EntityNotFoundException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Upsert/Modifier le rang d'un cheval dans le résultat")
|
||||
@PutMapping("/course/{courseId}/chevaux")
|
||||
public ResponseEntity<?> upsertLine(@PathVariable Long courseId,
|
||||
@RequestBody ResultatChevalDto dto) {
|
||||
try {
|
||||
ResultatCheval rc = resultatService.upsertResultatCheval(courseId, dto);
|
||||
return ResponseEntity.ok(rc);
|
||||
} catch (IllegalArgumentException | EntityNotFoundException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Supprimer une ligne (cheval) du résultat")
|
||||
@DeleteMapping("/course/{courseId}/chevaux/{chevalId}")
|
||||
public ResponseEntity<?> deleteLine(@PathVariable Long courseId, @PathVariable Long chevalId) {
|
||||
try {
|
||||
resultatService.deleteResultatCheval(courseId, chevalId);
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException | EntityNotFoundException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Numero+ (Quinté+) --------
|
||||
|
||||
@Operation(summary = "Fixer le Numéro+ gagnant (Quinté+)")
|
||||
@PatchMapping("/course/{courseId}/numero-plus")
|
||||
public ResponseEntity<?> setNumeroPlus(@PathVariable Long courseId,
|
||||
@RequestBody Map<String, String> body) {
|
||||
try {
|
||||
String numeroPlus = body.get("numeroPlusGagnant");
|
||||
if (numeroPlus == null || numeroPlus.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "numeroPlusGagnant is required"));
|
||||
}
|
||||
|
||||
Resultat r = resultatService.setNumeroPlus(courseId, numeroPlus);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"courseId", r.getCourse().getId(),
|
||||
"numeroPlusGagnant", r.getNumeroPlusGagnant()
|
||||
));
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "Tirage aléatoire du Numéro+ (Quinté+)")
|
||||
@PatchMapping("/course/{courseId}/numero-plus/draw")
|
||||
public ResponseEntity<?> drawNumeroPlus(@PathVariable Long courseId) {
|
||||
try {
|
||||
Resultat r = resultatService.drawNumeroPlus(courseId);
|
||||
return ResponseEntity.ok(Map.of("numeroPlusGagnant", r.getNumeroPlusGagnant()));
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Validation --------
|
||||
@Operation(summary = "Valider cohérence résultat (NP/DQ, Dead-Heat, etc.)")
|
||||
@GetMapping("/course/{courseId}/validate")
|
||||
public ResponseEntity<Map<String, Object>> validate(@PathVariable Long courseId) {
|
||||
return ResponseEntity.ok(resultatService.validate(courseId));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.pmumali.plr.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BulkChevalCourseRequest(List<ChevalEntry> entries) {
|
||||
public record BulkChevalCourseRequest(Long courseId, List<ChevalEntry> entries) {
|
||||
public static record ChevalEntry(Long chevalId, Integer numeroCheval) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
public record ChevalDto(Long id, String nom, Integer numero, String nomEcurie, Integer birthYear) {}
|
||||
public record ChevalDto(Long id, String nom, String nomEcurie, Integer birthYear) {
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ import java.time.LocalDateTime;
|
||||
|
||||
import com.pmumali.plr.enums.CourseStatue;
|
||||
|
||||
public record CourseDto(Long id, String nom, String lieu, LocalDateTime departureDateTime, CourseStatue status){}
|
||||
public record CourseDto(Long id, String nom, String lieu, LocalDateTime departureDateTime, Integer distance,
|
||||
CourseStatue status) {
|
||||
}
|
||||
32
src/main/java/com/pmumali/plr/dtos/CreatePariRequestDto.java
Normal file
32
src/main/java/com/pmumali/plr/dtos/CreatePariRequestDto.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreatePariRequestDto {
|
||||
|
||||
@NotNull(message = "Course ID cannot be null")
|
||||
private Long courseId;
|
||||
|
||||
@NotNull(message = "PariType cannot be null")
|
||||
private PariType pariType;
|
||||
|
||||
@NotNull(message = "Mise cannot be null")
|
||||
@DecimalMin(value = "0", message = "Minimum mise is 0")
|
||||
private BigDecimal mise;
|
||||
|
||||
@NotEmpty(message = "Bettor reference cannot be empty")
|
||||
private String bettorRef;
|
||||
|
||||
@NotEmpty(message = "Selections cannot be empty")
|
||||
private List<@Valid PariSelectionDto> selections;
|
||||
}
|
||||
28
src/main/java/com/pmumali/plr/dtos/GainViewDto.java
Normal file
28
src/main/java/com/pmumali/plr/dtos/GainViewDto.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record GainViewDto(
|
||||
Long pariId,
|
||||
Long courseId,
|
||||
String courseNom,
|
||||
PariType pariType,
|
||||
String pariSousType, // e.g. "QP_ORDRE", "MULTI6" (optional)
|
||||
String bettorRef,
|
||||
BigDecimal mise,
|
||||
BigDecimal gain, // official or 0 when provisional
|
||||
String payoutMode, // "OFFICIAL" or "PROVISIONAL"
|
||||
String numeroPlus, // for Quinté+ tickets (if any)
|
||||
List<GainViewDto.Selection> selections,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
@Builder
|
||||
public static record Selection(Long chevalId, String nomCheval, Integer position) {}
|
||||
}
|
||||
24
src/main/java/com/pmumali/plr/dtos/PariResponseDto.java
Normal file
24
src/main/java/com/pmumali/plr/dtos/PariResponseDto.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PariResponseDto {
|
||||
private Long id;
|
||||
private Long courseId;
|
||||
private String courseNom;
|
||||
private PariType pariType;
|
||||
private BigDecimal mise;
|
||||
private String bettorRef;
|
||||
private Boolean estGagnant;
|
||||
private BigDecimal gain;
|
||||
private LocalDateTime createdAt;
|
||||
private List<PariSelectionResponseDto> selections;
|
||||
}
|
||||
14
src/main/java/com/pmumali/plr/dtos/PariSelectionDto.java
Normal file
14
src/main/java/com/pmumali/plr/dtos/PariSelectionDto.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PariSelectionDto {
|
||||
@NotNull
|
||||
private Long chevalId;
|
||||
|
||||
@Min(1)
|
||||
private Integer position;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PariSelectionResponseDto {
|
||||
private Long chevalId;
|
||||
private String nomCheval;
|
||||
private Integer position;
|
||||
}
|
||||
16
src/main/java/com/pmumali/plr/dtos/RapportDto.java
Normal file
16
src/main/java/com/pmumali/plr/dtos/RapportDto.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class RapportDto {
|
||||
private Long courseId;
|
||||
private PariType pariType;
|
||||
private BigDecimal rapportParUnite; // cagnotte / total mise gagnants (0 si pas de gagnant)
|
||||
}
|
||||
15
src/main/java/com/pmumali/plr/dtos/ResultatChevalDto.java
Normal file
15
src/main/java/com/pmumali/plr/dtos/ResultatChevalDto.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ResultatChevalDto {
|
||||
@NotNull
|
||||
private Long chevalId;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
private Integer rang;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
|
||||
public record ResultatChevalView(ChevalDto cheval, Integer numeroCheval, Integer rang) {}
|
||||
21
src/main/java/com/pmumali/plr/dtos/ResultatCreationDto.java
Normal file
21
src/main/java/com/pmumali/plr/dtos/ResultatCreationDto.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ResultatCreationDto {
|
||||
@NotNull
|
||||
private Long courseId;
|
||||
|
||||
// optionnel pour Quinté+
|
||||
private String numeroPlusGagnant;
|
||||
|
||||
@NotEmpty
|
||||
@Valid
|
||||
private List<ResultatChevalDto> resultatsChevaux;
|
||||
}
|
||||
19
src/main/java/com/pmumali/plr/dtos/ResultatDto.java
Normal file
19
src/main/java/com/pmumali/plr/dtos/ResultatDto.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record ResultatDto(
|
||||
Long id,
|
||||
Long courseId,
|
||||
String courseNom,
|
||||
LocalDateTime dateOfficielle,
|
||||
String numeroPlusGagnant,
|
||||
List<LigneDto> arrivee
|
||||
) {
|
||||
@Builder
|
||||
public record LigneDto(Long chevalId, String chevalNom, Integer rang) {}
|
||||
}
|
||||
27
src/main/java/com/pmumali/plr/dtos/SettlementSummaryDto.java
Normal file
27
src/main/java/com/pmumali/plr/dtos/SettlementSummaryDto.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class SettlementSummaryDto {
|
||||
private Long courseId;
|
||||
private Map<PariType, PoolLine> pools; // par type
|
||||
private List<RapportDto> rapports; // par type (€/1 unité de mise)
|
||||
private boolean closed; // course cloturée ?
|
||||
|
||||
@Data @Builder
|
||||
public static class PoolLine {
|
||||
private BigDecimal totalMise; // total misé
|
||||
private BigDecimal prelevement; // montant prélevé
|
||||
private BigDecimal cagnotte; // totalMise - prelevement
|
||||
private BigDecimal totalMiseGagnants; // somme des mises gagnantes
|
||||
}
|
||||
}
|
||||
28
src/main/java/com/pmumali/plr/dtos/UpdatePariRequestDto.java
Normal file
28
src/main/java/com/pmumali/plr/dtos/UpdatePariRequestDto.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.pmumali.plr.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UpdatePariRequestDto {
|
||||
@NotNull
|
||||
private PariType pariType;
|
||||
|
||||
@NotNull @DecimalMin("0.01")
|
||||
private BigDecimal mise;
|
||||
|
||||
@NotBlank
|
||||
private String bettorRef;
|
||||
|
||||
@NotNull @Size(min = 1)
|
||||
private List<@Valid PariSelectionDto> selections;
|
||||
}
|
||||
8
src/main/java/com/pmumali/plr/enums/PariSousType.java
Normal file
8
src/main/java/com/pmumali/plr/enums/PariSousType.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package com.pmumali.plr.enums;
|
||||
|
||||
public enum PariSousType {
|
||||
// Quinté+
|
||||
QP_ORDRE, QP_DESORDRE, QP_BONUS4, QP_BONUS3,
|
||||
// Multi
|
||||
MULTI4, MULTI5, MULTI6, MULTI7
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
package com.pmumali.plr.enums;
|
||||
|
||||
public enum PariType {
|
||||
SIMPLE_GAGNANT, SIMPLE_PLACE
|
||||
SIMPLE_GAGNANT, // Simple Gagnant : Parier sur le cheval qui termine premier.
|
||||
SIMPLE_PLACE, // Simple Placé : Parier sur un cheval qui termine dans les 2 ou 3 premiers
|
||||
// (selon le nombre de partants).
|
||||
COUPLE_GAGNANT, // Couplé Gagnant : Parier sur les deux chevaux qui terminent premier et
|
||||
// deuxième, sans tenir compte de l'ordre.
|
||||
COUPLE_ORDRE, // Couplé Ordre : Parier sur les deux chevaux qui terminent premier et deuxième,
|
||||
// dans l'ordre exact.
|
||||
TRIO_ORDRE, // Trio Ordre : Parier sur les trois premiers chevaux, dans l'ordre exact.
|
||||
TRIO, // Trio : Parier sur les trois premiers chevaux, dans le désordre.
|
||||
TIERCE, // Tiercé : Parier sur les trois premiers chevaux, dans l'ordre ou le désordre.
|
||||
QUARTE, // Quarté : Parier sur les quatre premiers chevaux, dans l'ordre ou le désordre.
|
||||
QUINTE, // Quinté : Parier sur les cinq premiers chevaux, dans l'ordre ou le désordre,
|
||||
// avec un bonus pour le numéro de la tirelire.
|
||||
QUINTE_PLUS, // Quinté plus:
|
||||
MULTI, // Multi : Parier sur les chevaux qui terminent dans les 4 premiers d'une
|
||||
// course, en sélectionnant 4, 5, 6 ou 7 chevaux.
|
||||
DEUX_SUR_QUATRE, // 2sur4 : Parier sur deux chevaux qui terminent parmi les quatre premiers.
|
||||
PICK_CINQ // Pick 5 : Parier sur les cinq chevaux gagnants de cinq courses différentes.
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.Data;
|
||||
@@ -8,17 +7,14 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Cheval extends BaseEntite {
|
||||
private String nom;
|
||||
|
||||
private Integer numero;
|
||||
|
||||
private String nomEcurie;
|
||||
|
||||
@Column(name = "birth_year")
|
||||
|
||||
@@ -11,19 +11,20 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Course extends BaseEntite {
|
||||
private String nom;
|
||||
|
||||
private String lieu;
|
||||
|
||||
@Column(name="date_depart")
|
||||
@Column(name = "date_depart")
|
||||
private LocalDateTime departureDateTime;
|
||||
|
||||
private Integer distance;
|
||||
|
||||
private CourseStatue status = CourseStatue.PLANIFIEE;
|
||||
}
|
||||
|
||||
53
src/main/java/com/pmumali/plr/models/Jackpot.java
Normal file
53
src/main/java/com/pmumali/plr/models/Jackpot.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "jackpots", uniqueConstraints = @UniqueConstraint(columnNames = {"pari_type"}))
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
public class Jackpot extends BaseEntite {
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "pari_type", nullable = false)
|
||||
private PariType pariType;
|
||||
|
||||
@Column(nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal solde = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "last_updated", nullable = false)
|
||||
private LocalDateTime lastUpdated = LocalDateTime.now();
|
||||
|
||||
public Jackpot(PariType pariType, BigDecimal solde, LocalDateTime lastUpdated) {
|
||||
this.pariType = pariType;
|
||||
this.solde = (solde != null ? solde : BigDecimal.ZERO);
|
||||
this.lastUpdated = (lastUpdated != null ? lastUpdated : LocalDateTime.now());
|
||||
}
|
||||
|
||||
public void credit(BigDecimal amount) {
|
||||
if (amount != null && amount.signum() > 0) {
|
||||
solde = solde.add(amount);
|
||||
lastUpdated = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
public BigDecimal drain() {
|
||||
BigDecimal r = solde;
|
||||
solde = BigDecimal.ZERO;
|
||||
lastUpdated = LocalDateTime.now();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@@ -16,18 +23,36 @@ import lombok.Setter;
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Pari extends BaseEntite {
|
||||
@ManyToOne(optional=false)
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "course_id")
|
||||
private Course course;
|
||||
|
||||
@ManyToOne(optional=false)
|
||||
private Cheval cheval;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PariType pariType;
|
||||
|
||||
@Column(nullable=false)
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal mise;
|
||||
|
||||
@Column(name = "bettor_ref", nullable = false)
|
||||
private String bettorRef;
|
||||
|
||||
@Column(name = "est_gagnant")
|
||||
private Boolean estGagnant; // null = pending, false = lost, true = won
|
||||
|
||||
@Column(precision = 10, scale = 2)
|
||||
private BigDecimal gain;
|
||||
|
||||
// This is the major change: A Pari has many selections
|
||||
@OneToMany(mappedBy = "pari", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<PariSelection> selections = new ArrayList<>();
|
||||
|
||||
// Champs pour Quinté+ et Multi
|
||||
@Column(name = "numero_plus") // nullable si pas Quinté+
|
||||
private String numeroPlus;
|
||||
|
||||
@Column(name = "selection_count") // utile pour Multi : 4..7
|
||||
private Integer selectionCount;
|
||||
}
|
||||
|
||||
41
src/main/java/com/pmumali/plr/models/PariSelection.java
Normal file
41
src/main/java/com/pmumali/plr/models/PariSelection.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Table(name = "pari_selections", uniqueConstraints = {
|
||||
// A horse cannot be selected twice for the same bet
|
||||
@UniqueConstraint(columnNames = { "pari_id", "cheval_id" }),
|
||||
// For a single bet, each position can only be assigned once
|
||||
@UniqueConstraint(columnNames = { "pari_id", "position" })
|
||||
})
|
||||
public class PariSelection extends BaseEntite {
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "pari_id")
|
||||
private Pari pari;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "cheval_id")
|
||||
private Cheval cheval;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer position; // The order of the horse in the bet (1st, 2nd, etc.)
|
||||
}
|
||||
49
src/main/java/com/pmumali/plr/models/Resultat.java
Normal file
49
src/main/java/com/pmumali/plr/models/Resultat.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
|
||||
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "resultats")
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
|
||||
public class Resultat extends BaseEntite {
|
||||
@OneToOne
|
||||
@JoinColumn(name = "course_id", nullable = false, unique = true)
|
||||
private Course course;
|
||||
|
||||
@Column(name = "date_officielle", nullable = false)
|
||||
private LocalDateTime dateOfficielle = LocalDateTime.now();
|
||||
|
||||
@OneToMany(mappedBy = "resultat", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("rang ASC, id ASC")
|
||||
private List<ResultatCheval> resultatsChevaux = new ArrayList<>();;
|
||||
|
||||
// Gestion Quinté+ et Multi
|
||||
@Column(name = "numero_plus_gagnant")
|
||||
private String numeroPlusGagnant;
|
||||
}
|
||||
45
src/main/java/com/pmumali/plr/models/ResultatCheval.java
Normal file
45
src/main/java/com/pmumali/plr/models/ResultatCheval.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.pmumali.plr.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "resultats_chevaux", uniqueConstraints = @UniqueConstraint(columnNames = { "resultat_id", "cheval_id" }))
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
|
||||
public class ResultatCheval extends BaseEntite {
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "resultat_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private Resultat resultat;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "cheval_id", nullable = false)
|
||||
private Cheval cheval;
|
||||
|
||||
@Column(name = "rang", nullable = false)
|
||||
private Integer rang;
|
||||
|
||||
// Dead Heat is prevented by the constraints by fixing the resultat_id and
|
||||
// cheval_id
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.pmumali.plr.repositories;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.pmumali.plr.models.Cheval;
|
||||
import com.pmumali.plr.models.ChevalCourse;
|
||||
import com.pmumali.plr.models.Course;
|
||||
|
||||
@@ -11,6 +13,13 @@ public interface ChevalCourseRepository extends JpaRepository<ChevalCourse, Long
|
||||
// Retourne les chevaux non-partants pour une course
|
||||
List<ChevalCourse> findByCourseAndNonPartantTrue(Course course);
|
||||
|
||||
List<ChevalCourse> findByCourseIdAndChevalId(Long courseId, Long chevalId);
|
||||
|
||||
Optional<ChevalCourse> findByCourseAndChevalId(Course course, Long chevalId);
|
||||
|
||||
// Si tu veux aussi récupérer via l'entité Cheval
|
||||
Optional<ChevalCourse> findByCourseAndCheval(Course course, Cheval cheval);
|
||||
|
||||
// Retourne les chevaux disqualifiés pour une course
|
||||
List<ChevalCourse> findByCourseAndEstDisqualifieTrue(Course course);
|
||||
|
||||
@@ -35,4 +44,8 @@ public interface ChevalCourseRepository extends JpaRepository<ChevalCourse, Long
|
||||
boolean existsByCourseAndNumeroCheval(Course course, Integer numeroCheval);
|
||||
|
||||
int countByCourse(Course course);
|
||||
|
||||
List<ChevalCourse> findByCourse(Course course);
|
||||
|
||||
List<ChevalCourse> findByCourseId(Long courseId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.pmumali.plr.repositories;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.models.Jackpot;
|
||||
|
||||
public interface JackpotRepository extends JpaRepository<Jackpot, Long>{
|
||||
Optional<Jackpot> findByPariType(PariType pariType);
|
||||
}
|
||||
@@ -2,25 +2,53 @@ package com.pmumali.plr.repositories;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.models.Pari;
|
||||
|
||||
public interface PariRepository extends JpaRepository<Pari, Long> {
|
||||
List<Pari> findByCourseIdAndPariType(Long courseId, PariType pariType);
|
||||
List<Pari> findByCourseIdAndEstGagnantTrue(Long courseId);
|
||||
|
||||
List<Pari> findByCourseIdAndPariTypeAndChevalId(Long courseId, PariType pariType, Long chevalId);
|
||||
List<Pari> findByCourseIdAndPariTypeAndEstGagnantTrue(Long courseId, PariType pariType);
|
||||
|
||||
@EntityGraph(attributePaths = {"course", "selections", "selections.cheval"})
|
||||
@Override
|
||||
List<Pari> findAll();
|
||||
|
||||
@EntityGraph(attributePaths = {"course", "selections", "selections.cheval"})
|
||||
List<Pari> findByCourseId(Long courseId);
|
||||
|
||||
|
||||
@EntityGraph(attributePaths = {"course", "selections", "selections.cheval"})
|
||||
List<Pari> findByPariType(PariType pariType);
|
||||
|
||||
@Query("select coalesce(sum(p.mise),0) from Pari p where p.course.id=:courseId and p.pariType=:pariType")
|
||||
BigDecimal sumMiseByCourseIdAndPariType(Long courseId, PariType pariType);
|
||||
@EntityGraph(attributePaths = {"course", "selections", "selections.cheval"})
|
||||
List<Pari> findByCourseIdAndPariType(Long courseId, PariType pariType);
|
||||
|
||||
@Query("select coalesce(sum(p.mise),0) from Pari p where p.course.id=:courseId and p.pariType=:pariType and p.cheval.id=:chevalId")
|
||||
BigDecimal sumMiseByCourseIdAndPariTypeAndChevalId(Long courseId, PariType pariType, Long chevalId);
|
||||
@EntityGraph(attributePaths = {"course", "selections", "selections.cheval"})
|
||||
@Override
|
||||
Optional<Pari> findById(Long id);
|
||||
|
||||
@Query("SELECT p FROM Pari p JOIN p.selections s WHERE p.course.id = :courseId AND p.pariType = :pariType AND s.cheval.id = :chevalId")
|
||||
List<Pari> findByCourseAndTypeAndSelection(
|
||||
@Param("courseId") Long courseId,
|
||||
@Param("pariType") PariType pariType,
|
||||
@Param("chevalId") Long chevalId);
|
||||
|
||||
@Query("select coalesce(sum(p.mise), 0) from Pari p where p.course.id = :courseId and p.pariType = :pariType")
|
||||
BigDecimal sumMiseByCourseIdAndPariType(@Param("courseId") Long courseId, @Param("pariType") PariType pariType);
|
||||
|
||||
@Query("SELECT coalesce(sum(p.mise), 0) FROM Pari p JOIN p.selections s WHERE p.course.id = :courseId AND p.pariType = :pariType AND s.cheval.id = :chevalId")
|
||||
BigDecimal sumMiseForSelection(
|
||||
@Param("courseId") Long courseId,
|
||||
@Param("pariType") PariType pariType,
|
||||
@Param("chevalId") Long chevalId);
|
||||
|
||||
int countByCourseId(Long courseId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.pmumali.plr.repositories;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
import com.pmumali.plr.models.ResultatCheval;
|
||||
|
||||
public interface ResultatChevalRepository extends JpaRepository<ResultatCheval, Long> {
|
||||
List<ResultatCheval> findByResultatOrderByRangAsc(Resultat resultat);
|
||||
|
||||
List<ResultatCheval> findByResultat_Course_IdOrderByRangAsc(Long courseId);
|
||||
|
||||
List<ResultatCheval> findByResultatAndRang(Resultat resultat, Integer rang);
|
||||
|
||||
boolean existsByResultatAndChevalId(Resultat resultat, Long chevalId);
|
||||
|
||||
Optional<ResultatCheval> findByResultatAndChevalId(Resultat resultat, Long chevalId);
|
||||
|
||||
void deleteByResultatAndChevalId(Resultat resultat, Long chevalId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("delete from ResultatCheval rc where rc.resultat = :res")
|
||||
void deleteByResultat(@org.springframework.data.repository.query.Param("res") Resultat res);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("delete from ResultatCheval rc where rc.resultat.course.id = :courseId")
|
||||
void deleteByCourseId(@org.springframework.data.repository.query.Param("courseId") Long courseId);
|
||||
|
||||
// @Query("""
|
||||
// select rc
|
||||
// from ResultatCheval rc
|
||||
// join fetch rc.cheval ch
|
||||
// where rc.resultat.course.id = :courseId
|
||||
// order by rc.rang asc, rc.id asc
|
||||
// """)
|
||||
// List<ResultatCheval> findWithChevalByCourseIdOrderByRangAsc(Long courseId);
|
||||
|
||||
@Query("""
|
||||
select rc, cc.numeroCheval
|
||||
from ResultatCheval rc
|
||||
join rc.cheval ch
|
||||
join ChevalCourse cc on cc.cheval = ch and cc.course = rc.resultat.course
|
||||
where rc.resultat.course.id = :courseId
|
||||
order by rc.rang asc, rc.id asc
|
||||
""")
|
||||
List<Object[]> findWithChevalAndNumeroByCourseId(Long courseId);
|
||||
|
||||
// A) same query but with numeroCheval (via ChevalCourse) for DTO building:
|
||||
@Query("""
|
||||
select rc, cc.numeroCheval
|
||||
from ResultatCheval rc
|
||||
join rc.cheval ch
|
||||
join ChevalCourse cc on cc.cheval = ch and cc.course = rc.resultat.course
|
||||
where rc.resultat = :resultat
|
||||
order by rc.rang asc, rc.id asc
|
||||
""")
|
||||
List<Object[]> findWithNumeroByResultat(@Param("resultat") Resultat resultat);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.pmumali.plr.repositories;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
|
||||
public interface ResultatRepository extends JpaRepository<Resultat, Long> {
|
||||
Optional<Resultat> findByCourseId(Long courseId);
|
||||
}
|
||||
@@ -20,26 +20,25 @@ public class ChevalService {
|
||||
return chevalRepository.save(cheval);
|
||||
}
|
||||
|
||||
public List<Cheval> all(){
|
||||
public List<Cheval> all() {
|
||||
return chevalRepository.findAll();
|
||||
}
|
||||
|
||||
public Cheval get(Long id){
|
||||
public Cheval get(Long id) {
|
||||
return chevalRepository.findById(id).orElseThrow();
|
||||
}
|
||||
|
||||
public Cheval update(Long id, Cheval data){
|
||||
public Cheval update(Long id, Cheval data) {
|
||||
Cheval h = get(id);
|
||||
|
||||
h.setNom(data.getNom());
|
||||
h.setNumero(data.getNumero());
|
||||
h.setNomEcurie(data.getNomEcurie());
|
||||
h.setBirthYear(data.getBirthYear());
|
||||
|
||||
return chevalRepository.save(h);
|
||||
}
|
||||
|
||||
public void delete(Long id){
|
||||
public void delete(Long id) {
|
||||
chevalRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public class CourseService {
|
||||
existing.setLieu(data.getLieu());
|
||||
if (Objects.nonNull(data.getDepartureDateTime()))
|
||||
existing.setDepartureDateTime(data.getDepartureDateTime());
|
||||
if (Objects.nonNull(data.getDistance()))
|
||||
existing.setDistance(data.getDistance());
|
||||
if (Objects.nonNull(data.getStatus()))
|
||||
existing.setStatus(data.getStatus());
|
||||
|
||||
@@ -75,16 +77,9 @@ public class CourseService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteCourse(Long id, boolean hard) {
|
||||
public void deleteCourse(Long id) {
|
||||
Course course = get(id);
|
||||
|
||||
if (!hard) {
|
||||
// soft delete: mark as canceled
|
||||
course.setStatus(CourseStatue.ANNULEE);
|
||||
courseRepository.save(course);
|
||||
return;
|
||||
}
|
||||
|
||||
// hard delete: check constraints
|
||||
int pariCount = pariRepository.countByCourseId(id);
|
||||
int inscriptionCount = chevalCourseRepository.countByCourse(course);
|
||||
|
||||
288
src/main/java/com/pmumali/plr/services/GainService.java
Normal file
288
src/main/java/com/pmumali/plr/services/GainService.java
Normal file
@@ -0,0 +1,288 @@
|
||||
// com.pmumali.plr.services.WinnerService.java
|
||||
package com.pmumali.plr.services;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.pmumali.plr.dtos.GainViewDto;
|
||||
import com.pmumali.plr.enums.CourseStatue;
|
||||
import com.pmumali.plr.enums.PariSousType;
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.models.Course;
|
||||
import com.pmumali.plr.models.Pari;
|
||||
import com.pmumali.plr.models.PariSelection;
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
import com.pmumali.plr.models.ResultatCheval;
|
||||
import com.pmumali.plr.repositories.ChevalCourseRepository;
|
||||
import com.pmumali.plr.repositories.CourseRepository;
|
||||
import com.pmumali.plr.repositories.PariRepository;
|
||||
import com.pmumali.plr.repositories.ResultatChevalRepository;
|
||||
import com.pmumali.plr.repositories.ResultatRepository;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GainService {
|
||||
|
||||
private final PariRepository pariRepository;
|
||||
private final CourseRepository courseRepository;
|
||||
private final ResultatRepository resultatRepository;
|
||||
private final ResultatChevalRepository resultatChevalRepository;
|
||||
private final ChevalCourseRepository chevalCourseRepository;
|
||||
|
||||
// ====== PUBLIC API ======
|
||||
|
||||
/**
|
||||
* Winners for a course (all pari types).
|
||||
* If the course is CLOTUREE and paris were settled, returns OFFICIAL winners (estGagnant=true with stored gains).
|
||||
* Otherwise returns PROVISIONAL winners computed from official ranking (dead-heat aware) with gain=0.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<GainViewDto> listWinners(Long courseId) {
|
||||
Course course = mustGetCourse(courseId);
|
||||
if (course.getStatus() == CourseStatue.CLOTUREE && resultatRepository.findByCourseId(courseId).isPresent()) {
|
||||
// Prefer official winners if gains already computed (settlement executed)
|
||||
List<Pari> official = pariRepository.findByCourseIdAndEstGagnantTrue(courseId);
|
||||
if (!official.isEmpty()) {
|
||||
return official.stream().map(this::toWinnerDtoOfficial).toList();
|
||||
}
|
||||
}
|
||||
// Otherwise compute provisional from results (if any)
|
||||
return computeProvisionalWinners(courseId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Winners filtered by pari type.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<GainViewDto> listWinnersByType(Long courseId, PariType pariType) {
|
||||
Course course = mustGetCourse(courseId);
|
||||
if (course.getStatus() == CourseStatue.CLOTUREE && resultatRepository.findByCourseId(courseId).isPresent()) {
|
||||
List<Pari> official = pariRepository.findByCourseIdAndPariTypeAndEstGagnantTrue(courseId, pariType);
|
||||
if (!official.isEmpty()) {
|
||||
return official.stream().map(this::toWinnerDtoOfficial).toList();
|
||||
}
|
||||
}
|
||||
return computeProvisionalWinners(courseId, pariType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check for a single pari: is it a winner?
|
||||
* Uses official flag if available, otherwise computes provisional.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public GainViewDto isWinner(Long pariId) {
|
||||
Pari pari = pariRepository.findById(pariId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pari not found: " + pariId));
|
||||
Long courseId = pari.getCourse().getId();
|
||||
|
||||
// If officially settled and marked
|
||||
if (pari.getEstGagnant() != null) {
|
||||
return toWinnerDtoOfficial(pari);
|
||||
}
|
||||
|
||||
// Provisional compute (dead-heat aware)
|
||||
Map<Integer, Set<Long>> rankMap = buildRankToChevalSet(courseId);
|
||||
Set<Long> validChevaux = validParticipants(pari.getCourse().getId());
|
||||
boolean win = isWinningBetDeadHeat(pari.getPariType(), pari, rankMap, validChevaux);
|
||||
return toWinnerDtoProvisional(pari, win);
|
||||
}
|
||||
|
||||
// ====== PROVISIONAL COMPUTE ======
|
||||
|
||||
private List<GainViewDto> computeProvisionalWinners(Long courseId, PariType filter) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Resultat not found for course: " + courseId));
|
||||
|
||||
Map<Integer, Set<Long>> rankMap = buildRankToChevalSet(res);
|
||||
Set<Long> validChevaux = validParticipants(courseId);
|
||||
|
||||
List<Pari> paris = (filter == null)
|
||||
? pariRepository.findByCourseId(courseId)
|
||||
: pariRepository.findByCourseIdAndPariType(courseId, filter);
|
||||
|
||||
return paris.stream()
|
||||
.filter(p -> isWinningBetDeadHeat(p.getPariType(), p, rankMap, validChevaux))
|
||||
.map(p -> toWinnerDtoProvisional(p, true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ====== MAPPING ======
|
||||
|
||||
private GainViewDto toWinnerDtoOfficial(Pari p) {
|
||||
return GainViewDto.builder()
|
||||
.pariId(p.getId())
|
||||
.courseId(p.getCourse().getId())
|
||||
.courseNom(p.getCourse().getNom())
|
||||
.pariType(p.getPariType())
|
||||
.pariSousType(detectSousTypeLabel(p)) // optional label for QUINTE+/MULTI
|
||||
.bettorRef(p.getBettorRef())
|
||||
.mise(p.getMise())
|
||||
.gain(p.getGain() == null ? BigDecimal.ZERO : p.getGain())
|
||||
.payoutMode("OFFICIAL")
|
||||
.numeroPlus(p.getNumeroPlus())
|
||||
.selections(p.getSelections().stream()
|
||||
.map(s -> new GainViewDto.Selection(s.getCheval().getId(), s.getCheval().getNom(), s.getPosition()))
|
||||
.toList())
|
||||
.createdAt(p.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private GainViewDto toWinnerDtoProvisional(Pari p, boolean isWinner) {
|
||||
return GainViewDto.builder()
|
||||
.pariId(p.getId())
|
||||
.courseId(p.getCourse().getId())
|
||||
.courseNom(p.getCourse().getNom())
|
||||
.pariType(p.getPariType())
|
||||
.pariSousType(detectSousTypeLabel(p))
|
||||
.bettorRef(p.getBettorRef())
|
||||
.mise(p.getMise())
|
||||
.gain(isWinner ? BigDecimal.ZERO : BigDecimal.ZERO) // no official payout yet
|
||||
.payoutMode("PROVISIONAL")
|
||||
.numeroPlus(p.getNumeroPlus())
|
||||
.selections(p.getSelections().stream()
|
||||
.map(s -> new GainViewDto.Selection(s.getCheval().getId(), s.getCheval().getNom(), s.getPosition()))
|
||||
.toList())
|
||||
.createdAt(p.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String detectSousTypeLabel(Pari p) {
|
||||
// Small helper to label QP/MULTI tickets
|
||||
if (p.getPariType() == PariType.QUINTE_PLUS) {
|
||||
// If you store a computed category, map it; otherwise leave null
|
||||
return null;
|
||||
}
|
||||
if (p.getPariType() == PariType.MULTI) {
|
||||
int n = (p.getSelectionCount() != null) ? p.getSelectionCount() : (p.getSelections() != null ? p.getSelections().size() : 0);
|
||||
return switch (n) {
|
||||
case 4 -> PariSousType.MULTI4.name();
|
||||
case 5 -> PariSousType.MULTI5.name();
|
||||
case 6 -> PariSousType.MULTI6.name();
|
||||
case 7 -> PariSousType.MULTI7.name();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ====== DEAD-HEAT AWARE WIN LOGIC (same as you use in PariService) ======
|
||||
|
||||
private boolean isWinningBetDeadHeat(PariType type, Pari p,
|
||||
Map<Integer, Set<Long>> rankMap,
|
||||
Set<Long> validChevaux) {
|
||||
Map<Integer, Long> posToCheval = p.getSelections().stream()
|
||||
.collect(Collectors.toMap(PariSelection::getPosition, s -> s.getCheval().getId()));
|
||||
|
||||
Set<Long> ticketChevaux = p.getSelections().stream()
|
||||
.map(s -> s.getCheval().getId()).collect(Collectors.toSet());
|
||||
|
||||
if (!validChevaux.containsAll(ticketChevaux)) return false;
|
||||
|
||||
switch (type) {
|
||||
case SIMPLE_GAGNANT -> {
|
||||
Set<Long> firsts = rankMap.getOrDefault(1, Set.of());
|
||||
return ticketChevaux.size() == 1 && firsts.contains(ticketChevaux.iterator().next());
|
||||
}
|
||||
case SIMPLE_PLACE -> {
|
||||
int places = (validChevaux.size() <= 7) ? 2 : 3;
|
||||
Set<Long> placeSet = new LinkedHashSet<>();
|
||||
for (int r = 1; r <= places; r++) placeSet.addAll(rankMap.getOrDefault(r, Set.of()));
|
||||
return ticketChevaux.size() == 1 && placeSet.contains(ticketChevaux.iterator().next());
|
||||
}
|
||||
case COUPLE_GAGNANT -> {
|
||||
Set<Long> top2 = new LinkedHashSet<>();
|
||||
top2.addAll(rankMap.getOrDefault(1, Set.of()));
|
||||
top2.addAll(rankMap.getOrDefault(2, Set.of()));
|
||||
return ticketChevaux.size() == 2 && top2.containsAll(ticketChevaux);
|
||||
}
|
||||
case COUPLE_ORDRE -> {
|
||||
Long c1 = posToCheval.get(1);
|
||||
Long c2 = posToCheval.get(2);
|
||||
return c1 != null && c2 != null
|
||||
&& rankMap.getOrDefault(1, Set.of()).contains(c1)
|
||||
&& rankMap.getOrDefault(2, Set.of()).contains(c2);
|
||||
}
|
||||
case TRIO, TIERCE -> {
|
||||
Set<Long> top3 = topNWithDeadHeat(rankMap, 3);
|
||||
return ticketChevaux.size() == 3 && top3.containsAll(ticketChevaux);
|
||||
}
|
||||
case TRIO_ORDRE -> {
|
||||
Long c1 = posToCheval.get(1), c2 = posToCheval.get(2), c3 = posToCheval.get(3);
|
||||
return c1 != null && c2 != null && c3 != null
|
||||
&& rankMap.getOrDefault(1, Set.of()).contains(c1)
|
||||
&& rankMap.getOrDefault(2, Set.of()).contains(c2)
|
||||
&& rankMap.getOrDefault(3, Set.of()).contains(c3);
|
||||
}
|
||||
case QUARTE -> {
|
||||
Set<Long> top4 = topNWithDeadHeat(rankMap, 4);
|
||||
return ticketChevaux.size() == 4 && top4.containsAll(ticketChevaux);
|
||||
}
|
||||
case QUINTE -> {
|
||||
Set<Long> top5 = topNWithDeadHeat(rankMap, 5);
|
||||
return ticketChevaux.size() == 5 && top5.containsAll(ticketChevaux);
|
||||
}
|
||||
case DEUX_SUR_QUATRE -> {
|
||||
Set<Long> top4 = topNWithDeadHeat(rankMap, 4);
|
||||
long match = ticketChevaux.stream().filter(top4::contains).count();
|
||||
return match >= 2;
|
||||
}
|
||||
case MULTI -> {
|
||||
Set<Long> mustHave = topNWithDeadHeat(rankMap, 4);
|
||||
return ticketChevaux.containsAll(mustHave);
|
||||
}
|
||||
case QUINTE_PLUS, PICK_CINQ -> {
|
||||
// Winners are paid by your settlement engine (QP sous-types); for “provisional”
|
||||
// you can compute like QUINTE (désordre/ordre) if desired. Here: return false (neutral).
|
||||
return false;
|
||||
}
|
||||
default -> { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> topNWithDeadHeat(Map<Integer, Set<Long>> rankMap, int n) {
|
||||
Set<Long> out = new LinkedHashSet<>();
|
||||
for (int i = 1; i <= n; i++) {
|
||||
Set<Long> ids = rankMap.get(i);
|
||||
if (ids != null) out.addAll(ids);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<Integer, Set<Long>> buildRankToChevalSet(Long courseId) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Resultat not found for course: " + courseId));
|
||||
return buildRankToChevalSet(res);
|
||||
}
|
||||
|
||||
private Map<Integer, Set<Long>> buildRankToChevalSet(Resultat res) {
|
||||
return resultatChevalRepository.findByResultatOrderByRangAsc(res).stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
ResultatCheval::getRang,
|
||||
LinkedHashMap::new,
|
||||
Collectors.mapping(rc -> rc.getCheval().getId(), Collectors.toCollection(LinkedHashSet::new))
|
||||
));
|
||||
}
|
||||
|
||||
private Set<Long> validParticipants(Long courseId) {
|
||||
return chevalCourseRepository.findByCourseId(courseId).stream()
|
||||
.filter(cc -> !Boolean.TRUE.equals(cc.getNonPartant()) && !Boolean.TRUE.equals(cc.getEstDisqualifie()))
|
||||
.map(cc -> cc.getCheval().getId())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Course mustGetCourse(Long id) {
|
||||
return courseRepository.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Course not found: " + id));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,776 @@
|
||||
package com.pmumali.plr.services;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.pmumali.plr.dtos.CreatePariRequestDto;
|
||||
import com.pmumali.plr.dtos.PariResponseDto;
|
||||
import com.pmumali.plr.dtos.PariSelectionDto;
|
||||
import com.pmumali.plr.dtos.PariSelectionResponseDto;
|
||||
import com.pmumali.plr.dtos.RapportDto;
|
||||
import com.pmumali.plr.dtos.SettlementSummaryDto;
|
||||
import com.pmumali.plr.dtos.UpdatePariRequestDto;
|
||||
import com.pmumali.plr.enums.CourseStatue;
|
||||
import com.pmumali.plr.enums.PariSousType;
|
||||
import com.pmumali.plr.enums.PariType;
|
||||
import com.pmumali.plr.models.Cheval;
|
||||
import com.pmumali.plr.models.Course;
|
||||
import com.pmumali.plr.models.Jackpot;
|
||||
import com.pmumali.plr.models.Pari;
|
||||
import com.pmumali.plr.models.PariSelection;
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
import com.pmumali.plr.repositories.ChevalCourseRepository;
|
||||
import com.pmumali.plr.repositories.ChevalRepository;
|
||||
import com.pmumali.plr.repositories.CourseRepository;
|
||||
import com.pmumali.plr.repositories.JackpotRepository;
|
||||
import com.pmumali.plr.repositories.PariRepository;
|
||||
import com.pmumali.plr.repositories.ResultatChevalRepository;
|
||||
import com.pmumali.plr.repositories.ResultatRepository;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Service
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class PariService {
|
||||
private final PariRepository pariRepository;
|
||||
private final CourseRepository courseRepository;
|
||||
private final ChevalRepository chevalRepository;
|
||||
private final ChevalCourseRepository chevalCourseRepository;
|
||||
private final ResultatRepository resultatRepository;
|
||||
private final ResultatChevalRepository resultatChevalRepository;
|
||||
private final JackpotRepository jackpotRepository;
|
||||
|
||||
// --- config prélèvements (pourcentage) — à terme: application.yml
|
||||
private static final Map<PariType, BigDecimal> PRELEVEMENTS = Map.of(
|
||||
PariType.SIMPLE_GAGNANT, new BigDecimal("0.18"),
|
||||
PariType.SIMPLE_PLACE, new BigDecimal("0.18"),
|
||||
PariType.COUPLE_GAGNANT, new BigDecimal("0.20")
|
||||
);
|
||||
|
||||
// Quinté+ : affectation des parts de la cagnotte (après prélèvement)
|
||||
private static final Map<PariSousType, BigDecimal> QP_REPARTITION = Map.of(
|
||||
PariSousType.QP_ORDRE, new BigDecimal("0.50"),
|
||||
PariSousType.QP_DESORDRE, new BigDecimal("0.30"),
|
||||
PariSousType.QP_BONUS4, new BigDecimal("0.15"),
|
||||
PariSousType.QP_BONUS3, new BigDecimal("0.05")
|
||||
);
|
||||
|
||||
// Multi : une cagnotte par sous-type (chacun a sa propre cagnotte)
|
||||
private static final Map<PariSousType, BigDecimal> MULTI_PRELEV = Map.of(
|
||||
PariSousType.MULTI4, new BigDecimal("0.20"),
|
||||
PariSousType.MULTI5, new BigDecimal("0.20"),
|
||||
PariSousType.MULTI6, new BigDecimal("0.20"),
|
||||
PariSousType.MULTI7, new BigDecimal("0.20")
|
||||
);
|
||||
|
||||
private BigDecimal prelevementPour(PariType t) {
|
||||
return switch (t) {
|
||||
case QUINTE_PLUS -> new BigDecimal("0.22");
|
||||
case MULTI -> new BigDecimal("0.20"); // non utilisé si on ventile par sous-type
|
||||
default -> PRELEVEMENTS.getOrDefault(t, new BigDecimal("0.20"));
|
||||
};
|
||||
}
|
||||
|
||||
// ------------- CRUD -------------
|
||||
@Transactional
|
||||
public PariResponseDto createPari(CreatePariRequestDto requestDto) {
|
||||
validateMise(requestDto.getMise());
|
||||
Course course = mustGetCourse(requestDto.getCourseId());
|
||||
ensureBettable(course);
|
||||
|
||||
Pari pari = new Pari();
|
||||
pari.setCourse(course);
|
||||
pari.setPariType(requestDto.getPariType());
|
||||
pari.setMise(requestDto.getMise());
|
||||
pari.setBettorRef(requestDto.getBettorRef());
|
||||
|
||||
if (requestDto.getPariType() == PariType.QUINTE_PLUS) {
|
||||
pari.setNumeroPlus(String.format("%06d", new java.util.Random().nextInt(1_000_000)));
|
||||
} else {
|
||||
pari.setNumeroPlus(null);
|
||||
}
|
||||
|
||||
pari.setSelectionCount(requestDto.getSelections() != null ? requestDto.getSelections().size() : null);
|
||||
|
||||
List<PariSelection> selections = validateAndCreateSelections(requestDto, pari, course);
|
||||
pari.setSelections(selections);
|
||||
|
||||
Pari saved = pariRepository.save(pari);
|
||||
return mapToDto(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pari create(Long courseId, Long chevalId, PariType pariType, BigDecimal mise, String bettorRef) {
|
||||
Cheval cheval = chevalRepository.findById(chevalId).orElseThrow();
|
||||
Course course = courseRepository.findById(courseId).orElseThrow();
|
||||
public PariResponseDto updatePari(Long id, UpdatePariRequestDto dto) {
|
||||
validateMise(dto.getMise());
|
||||
Pari existing = pariRepository.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pari not found: " + id));
|
||||
|
||||
Pari p = new Pari();
|
||||
Course course = existing.getCourse();
|
||||
ensureBettable(course); // no update if not PLANIFIEE
|
||||
|
||||
p.setCheval(cheval);
|
||||
p.setCourse(course);
|
||||
p.setPariType(pariType);
|
||||
p.setMise(mise);
|
||||
p.setBettorRef(bettorRef);
|
||||
// Rebuild selections
|
||||
existing.setPariType(dto.getPariType());
|
||||
existing.setMise(dto.getMise());
|
||||
existing.setBettorRef(dto.getBettorRef());
|
||||
|
||||
return pariRepository.save(p);
|
||||
// Clear to avoid unique constraint (pari_id, cheval_id) before inserting new ones
|
||||
existing.getSelections().clear();
|
||||
pariRepository.saveAndFlush(existing);
|
||||
|
||||
List<PariSelection> newSelections = validateAndCreateSelections(
|
||||
toCreateDto(existing.getCourse().getId(), dto), existing, course);
|
||||
|
||||
existing.getSelections().addAll(newSelections);
|
||||
|
||||
Pari saved = pariRepository.save(existing);
|
||||
return mapToDto(saved);
|
||||
}
|
||||
|
||||
public Pari get(Long id) {
|
||||
return pariRepository.findById(id).orElseThrow();
|
||||
private CreatePariRequestDto toCreateDto(Long courseId, UpdatePariRequestDto dto){
|
||||
CreatePariRequestDto c = new CreatePariRequestDto();
|
||||
c.setCourseId(courseId);
|
||||
c.setPariType(dto.getPariType());
|
||||
c.setMise(dto.getMise());
|
||||
c.setBettorRef(dto.getBettorRef());
|
||||
c.setSelections(dto.getSelections());
|
||||
return c;
|
||||
}
|
||||
|
||||
public List<Pari> all() {
|
||||
return pariRepository.findAll();
|
||||
@Transactional
|
||||
public void deletePari(Long id) {
|
||||
Pari existing = pariRepository.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pari not found: " + id));
|
||||
ensureBettable(existing.getCourse());
|
||||
pariRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public List<Pari> getParisByPariType(PariType pariType) {
|
||||
return pariRepository.findByPariType(pariType);
|
||||
// ------------- READ -------------
|
||||
@Transactional(readOnly = true)
|
||||
public PariResponseDto getPariById(Long id) {
|
||||
Pari p = pariRepository.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pari not found with id: " + id));
|
||||
return mapToDto(p);
|
||||
}
|
||||
|
||||
public List<Pari> getParisByCourseAndType(Long courseId, PariType pariType) {
|
||||
return pariRepository.findByCourseIdAndPariType(courseId, pariType);
|
||||
@Transactional(readOnly = true)
|
||||
public List<PariResponseDto> all() {
|
||||
return pariRepository.findAll().stream().map(this::mapToDto).toList();
|
||||
}
|
||||
|
||||
public java.math.BigDecimal sumMiseByCourseIdAndPariType(Long courseId, PariType pariType) {
|
||||
@Transactional(readOnly = true)
|
||||
public List<PariResponseDto> getParisByPariType(PariType pariType) {
|
||||
return pariRepository.findByPariType(pariType).stream().map(this::mapToDto).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PariResponseDto> getParisByCourseAndType(Long courseId, PariType pariType) {
|
||||
return pariRepository.findByCourseIdAndPariType(courseId, pariType).stream().map(this::mapToDto).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal sumMiseByCourseIdAndPariType(Long courseId, PariType pariType) {
|
||||
return pariRepository.sumMiseByCourseIdAndPariType(courseId, pariType);
|
||||
}
|
||||
|
||||
// ------------- RÈGLEMENT -------------
|
||||
@Transactional
|
||||
public SettlementSummaryDto settleCourse(Long courseId) {
|
||||
Course course = mustGetCourse(courseId);
|
||||
Resultat resultat = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Resultat not found for course: " + courseId));
|
||||
|
||||
// Dead-Heat map: rank -> set of chevalIds
|
||||
Map<Integer, Set<Long>> rankMap = buildRankToChevalSet(courseId);
|
||||
|
||||
// participants valides (hors NP/DQ)
|
||||
Set<Long> validChevaux = chevalCourseRepository.findByCourse(course).stream()
|
||||
.filter(cc -> !Boolean.TRUE.equals(cc.getNonPartant()) && !Boolean.TRUE.equals(cc.getEstDisqualifie()))
|
||||
.map(cc -> cc.getCheval().getId())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<PariType, SettlementSummaryDto.PoolLine> poolMap = new LinkedHashMap<>();
|
||||
List<RapportDto> rapports = new ArrayList<>();
|
||||
|
||||
// pour chaque type présent dans la course
|
||||
List<Pari> paris = pariRepository.findByCourseId(courseId);
|
||||
Map<PariType, List<Pari>> byType = paris.stream().collect(Collectors.groupingBy(Pari::getPariType));
|
||||
|
||||
// ----- QUINTE_PLUS -----
|
||||
List<Pari> qp = byType.getOrDefault(PariType.QUINTE_PLUS, List.of());
|
||||
|
||||
if (!qp.isEmpty()) {
|
||||
BigDecimal totalQP = qp.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal prelevQP = totalQP.multiply(prelevementPour(PariType.QUINTE_PLUS)).setScale(2, RoundingMode.DOWN);
|
||||
BigDecimal cagnotteQP = totalQP.subtract(prelevQP).max(BigDecimal.ZERO);
|
||||
|
||||
Map<PariSousType, BigDecimal> envelope = new LinkedHashMap<>();
|
||||
for (var e : QP_REPARTITION.entrySet()) {
|
||||
envelope.put(e.getKey(), cagnotteQP.multiply(e.getValue()).setScale(2, RoundingMode.DOWN));
|
||||
}
|
||||
|
||||
Map<PariSousType, List<Pari>> winnersByCat = new EnumMap<>(PariSousType.class);
|
||||
for (Pari p : qp) {
|
||||
PariSousType cat = computeQuintePlusSousTypeDeadHeat(p, rankMap);
|
||||
if (cat != null) {
|
||||
winnersByCat.computeIfAbsent(cat, k -> new ArrayList<>()).add(p);
|
||||
}
|
||||
}
|
||||
|
||||
for (var cat : List.of(PariSousType.QP_ORDRE, PariSousType.QP_DESORDRE, PariSousType.QP_BONUS4, PariSousType.QP_BONUS3)) {
|
||||
List<Pari> winners = winnersByCat.getOrDefault(cat, List.of());
|
||||
BigDecimal bucket = envelope.getOrDefault(cat, BigDecimal.ZERO);
|
||||
BigDecimal miseGagnante = winners.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal rapport = (miseGagnante.signum()==0) ? BigDecimal.ZERO
|
||||
: bucket.divide(miseGagnante, 4, RoundingMode.HALF_UP);
|
||||
|
||||
for (Pari p : winners) {
|
||||
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.DOWN);
|
||||
p.setGain(gain);
|
||||
p.setEstGagnant(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Perdants QP
|
||||
Set<Long> paidIds = winnersByCat.values().stream().flatMap(List::stream).map(Pari::getId).collect(Collectors.toSet());
|
||||
for (Pari p : qp) {
|
||||
if (!paidIds.contains(p.getId())) { p.setGain(BigDecimal.ZERO); p.setEstGagnant(false); }
|
||||
}
|
||||
|
||||
// TIRELIRE : Ordre + NumeroPlus
|
||||
Jackpot jp = jackpotRepository.findByPariType(PariType.QUINTE_PLUS)
|
||||
.orElseGet(() -> {
|
||||
Jackpot j = new Jackpot();
|
||||
j.setPariType(PariType.QUINTE_PLUS);
|
||||
j.setSolde(BigDecimal.ZERO);
|
||||
j.setLastUpdated(java.time.LocalDateTime.now());
|
||||
return jackpotRepository.save(j);
|
||||
});
|
||||
|
||||
String drawn = resultat.getNumeroPlusGagnant();
|
||||
List<Pari> ordreWinners = winnersByCat.getOrDefault(PariSousType.QP_ORDRE, List.of());
|
||||
List<Pari> plusWinners = (drawn == null) ? List.of()
|
||||
: ordreWinners.stream().filter(p -> drawn.equals(p.getNumeroPlus())).toList();
|
||||
|
||||
if (!plusWinners.isEmpty() && jp.getSolde().signum() > 0) {
|
||||
BigDecimal totalMisePlus = plusWinners.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (totalMisePlus.signum() > 0) {
|
||||
BigDecimal jackpot = jp.getSolde();
|
||||
jp.setSolde(BigDecimal.ZERO);
|
||||
jp.setLastUpdated(java.time.LocalDateTime.now());
|
||||
|
||||
for (Pari p : plusWinners) {
|
||||
BigDecimal part = p.getMise().divide(totalMisePlus, 8, RoundingMode.HALF_UP);
|
||||
BigDecimal bonus = jackpot.multiply(part).setScale(2, RoundingMode.DOWN);
|
||||
p.setGain(p.getGain().add(bonus));
|
||||
}
|
||||
jackpotRepository.save(jp);
|
||||
}
|
||||
} else {
|
||||
// créditer la tirelire d'une part (ex: 5% du totalQP)
|
||||
BigDecimal aliment = totalQP.multiply(new BigDecimal("0.05")).setScale(2, RoundingMode.DOWN);
|
||||
jp.setSolde(jp.getSolde().add(aliment));
|
||||
jp.setLastUpdated(java.time.LocalDateTime.now());
|
||||
jackpotRepository.save(jp);
|
||||
}
|
||||
|
||||
pariRepository.saveAll(qp);
|
||||
|
||||
// Pool line
|
||||
poolMap.put(PariType.QUINTE_PLUS, SettlementSummaryDto.PoolLine.builder()
|
||||
.totalMise(totalQP).prelevement(prelevQP).cagnotte(cagnotteQP)
|
||||
.totalMiseGagnants(
|
||||
qp.stream().filter(b -> Boolean.TRUE.equals(b.getEstGagnant()))
|
||||
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add))
|
||||
.build());
|
||||
|
||||
// Rapports (€/€ misé) indicatifs
|
||||
for (var cat : QP_REPARTITION.keySet()) {
|
||||
List<Pari> winners = winnersByCat.getOrDefault(cat, List.of());
|
||||
BigDecimal bucket = envelope.getOrDefault(cat, BigDecimal.ZERO);
|
||||
BigDecimal miseG = winners.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal rapport = (miseG.signum()==0) ? BigDecimal.ZERO : bucket.divide(miseG, 4, RoundingMode.HALF_UP);
|
||||
rapports.add(RapportDto.builder()
|
||||
.courseId(courseId).pariType(PariType.QUINTE_PLUS)
|
||||
.rapportParUnite(rapport.setScale(2, RoundingMode.DOWN))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// ----- MULTI -----
|
||||
List<Pari> multi = byType.getOrDefault(PariType.MULTI, List.of());
|
||||
|
||||
if (!multi.isEmpty()) {
|
||||
Map<PariSousType, List<Pari>> bySub = multi.stream()
|
||||
.collect(Collectors.groupingBy(this::computeMultiSousType, () -> new EnumMap<>(PariSousType.class), Collectors.toList()));
|
||||
|
||||
for (var sub : List.of(PariSousType.MULTI4, PariSousType.MULTI5, PariSousType.MULTI6, PariSousType.MULTI7)) {
|
||||
List<Pari> bucketBets = bySub.getOrDefault(sub, List.of());
|
||||
if (bucketBets.isEmpty()) continue;
|
||||
|
||||
BigDecimal total = bucketBets.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal prelev = total.multiply(MULTI_PRELEV.getOrDefault(sub, prelevementPour(PariType.MULTI))).setScale(2, RoundingMode.DOWN);
|
||||
BigDecimal cagnotte = total.subtract(prelev).max(BigDecimal.ZERO);
|
||||
|
||||
// gagnants = tickets qui contiennent tous les chevaux classés dans les 4 premiers (Dead-Heat)
|
||||
List<Pari> winners = bucketBets.stream().filter(p -> isMultiWinningDeadHeat(p, rankMap)).toList();
|
||||
BigDecimal miseG = winners.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal rapport = (miseG.signum()==0) ? BigDecimal.ZERO
|
||||
: cagnotte.divide(miseG, 4, RoundingMode.HALF_UP);
|
||||
|
||||
for (Pari p : bucketBets) {
|
||||
if (winners.contains(p)) {
|
||||
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.DOWN);
|
||||
p.setGain(gain);
|
||||
p.setEstGagnant(true);
|
||||
} else {
|
||||
p.setGain(BigDecimal.ZERO);
|
||||
p.setEstGagnant(false);
|
||||
}
|
||||
}
|
||||
pariRepository.saveAll(bucketBets);
|
||||
|
||||
// Agréger au niveau type MULTI
|
||||
poolMap.merge(PariType.MULTI,
|
||||
SettlementSummaryDto.PoolLine.builder()
|
||||
.totalMise(total)
|
||||
.prelevement(prelev)
|
||||
.cagnotte(cagnotte)
|
||||
.totalMiseGagnants(miseG)
|
||||
.build(),
|
||||
(a,b) -> SettlementSummaryDto.PoolLine.builder()
|
||||
.totalMise(a.getTotalMise().add(b.getTotalMise()))
|
||||
.prelevement(a.getPrelevement().add(b.getPrelevement()))
|
||||
.cagnotte(a.getCagnotte().add(b.getCagnotte()))
|
||||
.totalMiseGagnants(a.getTotalMiseGagnants().add(b.getTotalMiseGagnants()))
|
||||
.build());
|
||||
|
||||
rapports.add(RapportDto.builder()
|
||||
.courseId(courseId)
|
||||
.pariType(PariType.MULTI)
|
||||
.rapportParUnite(rapport.setScale(2, RoundingMode.DOWN))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// ----- AUTRES TYPES (hors QUINTE_PLUS & MULTI déjà traités) -----
|
||||
for (var entry : byType.entrySet()) {
|
||||
PariType type = entry.getKey();
|
||||
if (type == PariType.QUINTE_PLUS || type == PariType.MULTI) continue; // déjà payé ci-dessus
|
||||
|
||||
List<Pari> bets = entry.getValue();
|
||||
|
||||
BigDecimal totalMise = bets.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal prelev = totalMise.multiply(prelevementPour(type)).setScale(2, RoundingMode.DOWN);
|
||||
BigDecimal cagnotte = totalMise.subtract(prelev).max(BigDecimal.ZERO);
|
||||
|
||||
// gagnants (Dead-Heat aware)
|
||||
List<Pari> gagnants = bets.stream()
|
||||
.filter(b -> isWinningBetDeadHeat(type, b, rankMap, validChevaux))
|
||||
.toList();
|
||||
|
||||
BigDecimal miseGagnante = gagnants.stream()
|
||||
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal rapport = (miseGagnante.signum() == 0)
|
||||
? BigDecimal.ZERO
|
||||
: cagnotte.divide(miseGagnante, 4, RoundingMode.HALF_UP);
|
||||
|
||||
for (Pari p : bets) {
|
||||
if (gagnants.contains(p)) {
|
||||
BigDecimal gain = p.getMise().multiply(rapport).setScale(2, RoundingMode.DOWN);
|
||||
p.setGain(gain);
|
||||
p.setEstGagnant(true);
|
||||
} else {
|
||||
p.setGain(BigDecimal.ZERO);
|
||||
p.setEstGagnant(false);
|
||||
}
|
||||
}
|
||||
pariRepository.saveAll(bets);
|
||||
|
||||
poolMap.put(type, SettlementSummaryDto.PoolLine.builder()
|
||||
.totalMise(totalMise)
|
||||
.prelevement(prelev)
|
||||
.cagnotte(cagnotte)
|
||||
.totalMiseGagnants(miseGagnante)
|
||||
.build());
|
||||
|
||||
rapports.add(RapportDto.builder()
|
||||
.courseId(courseId)
|
||||
.pariType(type)
|
||||
.rapportParUnite(rapport.setScale(2, RoundingMode.DOWN))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Clôturer la course
|
||||
if (course.getStatus() != CourseStatue.CLOTUREE) {
|
||||
course.setStatus(CourseStatue.CLOTUREE);
|
||||
courseRepository.save(course);
|
||||
}
|
||||
|
||||
return SettlementSummaryDto.builder()
|
||||
.courseId(courseId)
|
||||
.pools(poolMap)
|
||||
.rapports(rapports)
|
||||
.closed(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public SettlementSummaryDto getSettlementReport(Long courseId) {
|
||||
Course course = mustGetCourse(courseId);
|
||||
List<Pari> paris = pariRepository.findByCourseId(courseId);
|
||||
Map<PariType, List<Pari>> byType = paris.stream().collect(Collectors.groupingBy(Pari::getPariType));
|
||||
|
||||
Map<PariType, SettlementSummaryDto.PoolLine> pools = new LinkedHashMap<>();
|
||||
List<RapportDto> rapports = new ArrayList<>();
|
||||
|
||||
for (var e : byType.entrySet()) {
|
||||
PariType t = e.getKey();
|
||||
List<Pari> bets = e.getValue();
|
||||
BigDecimal total = bets.stream().map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal prelev = total.multiply(prelevementPour(t)).setScale(2, RoundingMode.DOWN);
|
||||
BigDecimal cagnotte = total.subtract(prelev).max(BigDecimal.ZERO);
|
||||
BigDecimal miseG = bets.stream().filter(b -> Boolean.TRUE.equals(b.getEstGagnant()))
|
||||
.map(Pari::getMise).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
pools.put(t, SettlementSummaryDto.PoolLine.builder()
|
||||
.totalMise(total).prelevement(prelev).cagnotte(cagnotte).totalMiseGagnants(miseG).build());
|
||||
|
||||
BigDecimal rapport = (miseG.signum()==0) ? BigDecimal.ZERO :
|
||||
cagnotte.divide(miseG, 4, RoundingMode.HALF_UP);
|
||||
rapports.add(RapportDto.builder()
|
||||
.courseId(courseId).pariType(t).rapportParUnite(rapport.setScale(2, RoundingMode.DOWN)).build());
|
||||
}
|
||||
|
||||
return SettlementSummaryDto.builder()
|
||||
.courseId(courseId)
|
||||
.pools(pools)
|
||||
.rapports(rapports)
|
||||
.closed(course.getStatus()==CourseStatue.CLOTUREE)
|
||||
.build();
|
||||
}
|
||||
|
||||
// --------- Helpers & matching ----------
|
||||
private void validateMise(BigDecimal mise){
|
||||
if (mise == null || mise.signum() <= 0) throw new IllegalArgumentException("La mise doit être > 0.");
|
||||
}
|
||||
|
||||
private Course mustGetCourse(Long id){
|
||||
return courseRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Course not found: " + id));
|
||||
}
|
||||
|
||||
private void ensureBettable(Course c){
|
||||
if (c.getStatus() != CourseStatue.PLANIFIEE)
|
||||
throw new IllegalStateException("Opération interdite: la course n'est pas en statut PLANIFIEE.");
|
||||
}
|
||||
|
||||
private PariResponseDto mapToDto(Pari pari) {
|
||||
var selectionDtos = pari.getSelections().stream()
|
||||
.map(s -> PariSelectionResponseDto.builder()
|
||||
.chevalId(s.getCheval().getId())
|
||||
.nomCheval(s.getCheval().getNom())
|
||||
.position(s.getPosition())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
return PariResponseDto.builder()
|
||||
.id(pari.getId())
|
||||
.courseId(pari.getCourse().getId())
|
||||
.courseNom(pari.getCourse().getNom())
|
||||
.pariType(pari.getPariType())
|
||||
.mise(pari.getMise())
|
||||
.bettorRef(pari.getBettorRef())
|
||||
.estGagnant(pari.getEstGagnant())
|
||||
.gain(pari.getGain())
|
||||
.createdAt(pari.getCreatedAt())
|
||||
.selections(selectionDtos)
|
||||
.build();
|
||||
}
|
||||
|
||||
// --- VALIDATION selections existante (ta version, consolidée) ---
|
||||
private List<PariSelection> validateAndCreateSelections(CreatePariRequestDto requestDto, Pari pari, Course course) {
|
||||
PariType pariType = requestDto.getPariType();
|
||||
List<PariSelectionDto> selDtos = requestDto.getSelections();
|
||||
|
||||
if (selDtos == null || selDtos.isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins une sélection est requise.");
|
||||
}
|
||||
|
||||
int minExpected;
|
||||
int maxExpected;
|
||||
boolean positionsMustBeFullRange = false;
|
||||
|
||||
switch (pariType) {
|
||||
case SIMPLE_GAGNANT, SIMPLE_PLACE -> minExpected = maxExpected = 1;
|
||||
case COUPLE_GAGNANT -> minExpected = maxExpected = 2;
|
||||
case COUPLE_ORDRE -> { minExpected = maxExpected = 2; positionsMustBeFullRange = true; }
|
||||
case TRIO_ORDRE -> { minExpected = maxExpected = 3; positionsMustBeFullRange = true; }
|
||||
case TRIO, TIERCE -> minExpected = maxExpected = 3;
|
||||
case QUARTE -> minExpected = maxExpected = 4;
|
||||
case QUINTE, QUINTE_PLUS -> minExpected = maxExpected = 5;
|
||||
case MULTI -> { minExpected = 4; maxExpected = 7; }
|
||||
case DEUX_SUR_QUATRE -> minExpected = maxExpected = 2;
|
||||
case PICK_CINQ -> minExpected = maxExpected = 5;
|
||||
default -> throw new IllegalArgumentException("Type de pari non supporté: " + pariType);
|
||||
}
|
||||
|
||||
int provided = selDtos.size();
|
||||
if (provided < minExpected || provided > maxExpected) {
|
||||
String attendu = (minExpected == maxExpected) ? String.valueOf(minExpected) : (minExpected + " à " + maxExpected);
|
||||
throw new IllegalArgumentException(
|
||||
"Nombre de sélections invalide pour " + pariType + " : " + attendu + " attendu(s) mais " + provided + " fourni(s)"
|
||||
+ (positionsMustBeFullRange ? " (positions 1.." + provided + " requises)" : "")
|
||||
);
|
||||
}
|
||||
|
||||
Set<Long> chevalIdSet = new java.util.HashSet<>();
|
||||
for (PariSelectionDto s : selDtos) {
|
||||
if (!chevalIdSet.add(s.getChevalId())) {
|
||||
throw new IllegalArgumentException("Duplication de chevalId dans les sélections: " + s.getChevalId());
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Long> posToCheval = new java.util.HashMap<>();
|
||||
boolean anyPositionProvided = false;
|
||||
for (PariSelectionDto s : selDtos) {
|
||||
Integer pos = s.getPosition();
|
||||
if (pos != null) {
|
||||
anyPositionProvided = true;
|
||||
if (pos <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"La position doit être un entier positif. Position invalide pour chevalId " + s.getChevalId() + " : " + pos
|
||||
);
|
||||
}
|
||||
if (posToCheval.containsKey(pos)) {
|
||||
throw new IllegalArgumentException("Position dupliquée fournie : " + pos);
|
||||
}
|
||||
posToCheval.put(pos, s.getChevalId());
|
||||
}
|
||||
}
|
||||
|
||||
if (positionsMustBeFullRange) {
|
||||
int expectedN = provided;
|
||||
if (!anyPositionProvided) {
|
||||
throw new IllegalArgumentException(
|
||||
"Les positions sont requises pour le pari ordonné " + pariType + ". Fournir les positions 1.." + expectedN
|
||||
);
|
||||
}
|
||||
for (int i = 1; i <= expectedN; i++) {
|
||||
if (!posToCheval.containsKey(i)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Le pari ordonné " + pariType + " nécessite toutes les positions de 1 à " + expectedN + ". Position manquante : " + i
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PariSelection> resultSelections = new ArrayList<>(provided);
|
||||
int assignPosCounter = 1;
|
||||
|
||||
for (PariSelectionDto s : selDtos) {
|
||||
Long chevalId = s.getChevalId();
|
||||
|
||||
var chevalCourseOpt = chevalCourseRepository.findByCourseAndChevalId(course, chevalId);
|
||||
var chevalCourse = chevalCourseOpt.orElseThrow(
|
||||
() -> new jakarta.persistence.EntityNotFoundException(
|
||||
"Cheval avec id " + chevalId + " non inscrit à la course " + course.getId()
|
||||
)
|
||||
);
|
||||
|
||||
if (Boolean.TRUE.equals(chevalCourse.getNonPartant())) {
|
||||
throw new IllegalStateException(
|
||||
"Cheval id " + chevalId + " est marqué Non-Partant pour la course " + course.getId()
|
||||
);
|
||||
}
|
||||
if (Boolean.TRUE.equals(chevalCourse.getEstDisqualifie())) {
|
||||
throw new IllegalStateException(
|
||||
"Cheval id " + chevalId + " est Disqualifié pour la course " + course.getId()
|
||||
);
|
||||
}
|
||||
|
||||
PariSelection selection = new PariSelection();
|
||||
selection.setPari(pari);
|
||||
selection.setCheval(chevalCourse.getCheval());
|
||||
|
||||
Integer providedPos = s.getPosition();
|
||||
if (providedPos != null) {
|
||||
selection.setPosition(providedPos);
|
||||
} else {
|
||||
selection.setPosition(assignPosCounter++);
|
||||
}
|
||||
|
||||
resultSelections.add(selection);
|
||||
}
|
||||
|
||||
Set<Integer> finalPositions = new java.util.HashSet<>();
|
||||
for (PariSelection ps : resultSelections) {
|
||||
if (!finalPositions.add(ps.getPosition())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Positions dupliquées dans les sélections finalisées : " + ps.getPosition()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return resultSelections;
|
||||
}
|
||||
|
||||
// --- Dead-Heat matching helpers ---
|
||||
private Map<Integer, Set<Long>> buildRankToChevalSet(Long courseId) {
|
||||
var res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new jakarta.persistence.EntityNotFoundException("Resultat not found for course: " + courseId));
|
||||
|
||||
return resultatChevalRepository.findByResultatOrderByRangAsc(res).stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
rc -> rc.getRang(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.mapping(rc -> rc.getCheval().getId(), Collectors.toCollection(LinkedHashSet::new))
|
||||
));
|
||||
}
|
||||
|
||||
private Set<Long> topNWithDeadHeat(Map<Integer, Set<Long>> rankMap, int n) {
|
||||
Set<Long> out = new LinkedHashSet<>();
|
||||
for (int i = 1; i <= n; i++) {
|
||||
Set<Long> ids = rankMap.get(i);
|
||||
if (ids != null) out.addAll(ids);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private boolean isMultiWinningDeadHeat(Pari p, Map<Integer, Set<Long>> rankMap) {
|
||||
Set<Long> pick = p.getSelections().stream().map(s -> s.getCheval().getId()).collect(Collectors.toSet());
|
||||
Set<Long> top4 = topNWithDeadHeat(rankMap, 4);
|
||||
return pick.containsAll(top4);
|
||||
}
|
||||
|
||||
private boolean isWinningBetDeadHeat(PariType type, Pari p,
|
||||
Map<Integer, Set<Long>> rankMap,
|
||||
Set<Long> validChevaux) {
|
||||
|
||||
Map<Integer, Long> posToCheval = p.getSelections().stream()
|
||||
.collect(Collectors.toMap(PariSelection::getPosition, s -> s.getCheval().getId()));
|
||||
|
||||
Set<Long> ticketChevaux = p.getSelections().stream()
|
||||
.map(s -> s.getCheval().getId()).collect(Collectors.toSet());
|
||||
|
||||
if (!validChevaux.containsAll(ticketChevaux)) return false;
|
||||
|
||||
switch (type) {
|
||||
case SIMPLE_GAGNANT -> {
|
||||
Set<Long> firsts = rankMap.getOrDefault(1, Set.of());
|
||||
return ticketChevaux.size() == 1 && firsts.contains(ticketChevaux.iterator().next());
|
||||
}
|
||||
case SIMPLE_PLACE -> {
|
||||
int places = (validChevaux.size() <= 7) ? 2 : 3;
|
||||
Set<Long> placeSet = new LinkedHashSet<>();
|
||||
for (int r = 1; r <= places; r++) {
|
||||
placeSet.addAll(rankMap.getOrDefault(r, Set.of()));
|
||||
}
|
||||
return ticketChevaux.size() == 1 && placeSet.contains(ticketChevaux.iterator().next());
|
||||
}
|
||||
case COUPLE_GAGNANT -> {
|
||||
Set<Long> top2 = new LinkedHashSet<>();
|
||||
top2.addAll(rankMap.getOrDefault(1, Set.of()));
|
||||
top2.addAll(rankMap.getOrDefault(2, Set.of()));
|
||||
return ticketChevaux.size() == 2 && top2.containsAll(ticketChevaux);
|
||||
}
|
||||
case COUPLE_ORDRE -> {
|
||||
Long c1 = posToCheval.get(1);
|
||||
Long c2 = posToCheval.get(2);
|
||||
return c1 != null && c2 != null
|
||||
&& rankMap.getOrDefault(1, Set.of()).contains(c1)
|
||||
&& rankMap.getOrDefault(2, Set.of()).contains(c2);
|
||||
}
|
||||
case TRIO, TIERCE -> {
|
||||
Set<Long> top3 = topNWithDeadHeat(rankMap, 3);
|
||||
return ticketChevaux.size() == 3 && top3.containsAll(ticketChevaux);
|
||||
}
|
||||
case TRIO_ORDRE -> {
|
||||
Long c1 = posToCheval.get(1);
|
||||
Long c2 = posToCheval.get(2);
|
||||
Long c3 = posToCheval.get(3);
|
||||
return c1 != null && c2 != null && c3 != null
|
||||
&& rankMap.getOrDefault(1, Set.of()).contains(c1)
|
||||
&& rankMap.getOrDefault(2, Set.of()).contains(c2)
|
||||
&& rankMap.getOrDefault(3, Set.of()).contains(c3);
|
||||
}
|
||||
case QUARTE -> {
|
||||
Set<Long> top4 = topNWithDeadHeat(rankMap, 4);
|
||||
return ticketChevaux.size() == 4 && top4.containsAll(ticketChevaux);
|
||||
}
|
||||
case QUINTE -> {
|
||||
Set<Long> top5 = topNWithDeadHeat(rankMap, 5);
|
||||
return ticketChevaux.size() == 5 && top5.containsAll(ticketChevaux);
|
||||
}
|
||||
case DEUX_SUR_QUATRE -> {
|
||||
Set<Long> top4 = topNWithDeadHeat(rankMap, 4);
|
||||
long match = ticketChevaux.stream().filter(top4::contains).count();
|
||||
return match >= 2;
|
||||
}
|
||||
case MULTI -> {
|
||||
Set<Long> mustHave = topNWithDeadHeat(rankMap, 4);
|
||||
return ticketChevaux.containsAll(mustHave);
|
||||
}
|
||||
case QUINTE_PLUS, PICK_CINQ -> {
|
||||
return false;
|
||||
}
|
||||
default -> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quinté+ category with Dead-Heat
|
||||
private PariSousType computeQuintePlusSousTypeDeadHeat(Pari p, Map<Integer, Set<Long>> rankMap) {
|
||||
if (matchOrdreSet(p, rankMap, 5)) return PariSousType.QP_ORDRE;
|
||||
if (matchDesordreSet(p, rankMap, 5)) return PariSousType.QP_DESORDRE;
|
||||
if (matchKofNSet(p, rankMap, 4, 5)) return PariSousType.QP_BONUS4;
|
||||
if (matchKofNSet(p, rankMap, 3, 5)) return PariSousType.QP_BONUS3;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchOrdreSet(Pari p, Map<Integer, Set<Long>> rankMap, int n) {
|
||||
Map<Integer, Long> pos = p.getSelections().stream()
|
||||
.collect(Collectors.toMap(PariSelection::getPosition, s -> s.getCheval().getId()));
|
||||
for (int i = 1; i <= n; i++) {
|
||||
Long pick = pos.get(i);
|
||||
if (pick == null || !rankMap.getOrDefault(i, Set.of()).contains(pick)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean matchDesordreSet(Pari p, Map<Integer, Set<Long>> rankMap, int n) {
|
||||
Set<Long> pick = p.getSelections().stream().map(s -> s.getCheval().getId()).collect(Collectors.toSet());
|
||||
Set<Long> topN = topNWithDeadHeat(rankMap, n);
|
||||
return pick.size()==n && topN.containsAll(pick);
|
||||
}
|
||||
|
||||
private boolean matchKofNSet(Pari p, Map<Integer, Set<Long>> rankMap, int k, int n) {
|
||||
Set<Long> pick = p.getSelections().stream().map(s -> s.getCheval().getId()).collect(Collectors.toSet());
|
||||
Set<Long> topN = topNWithDeadHeat(rankMap, n);
|
||||
long matched = pick.stream().filter(topN::contains).count();
|
||||
return matched >= k;
|
||||
}
|
||||
|
||||
private PariSousType computeMultiSousType(Pari p) {
|
||||
Integer nObj = p.getSelectionCount();
|
||||
int n = (nObj != null) ? nObj : (p.getSelections() != null ? p.getSelections().size() : 0);
|
||||
return switch (n) {
|
||||
case 4 -> PariSousType.MULTI4;
|
||||
case 5 -> PariSousType.MULTI5;
|
||||
case 6 -> PariSousType.MULTI6;
|
||||
case 7 -> PariSousType.MULTI7;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
386
src/main/java/com/pmumali/plr/services/ResultatService.java
Normal file
386
src/main/java/com/pmumali/plr/services/ResultatService.java
Normal file
@@ -0,0 +1,386 @@
|
||||
package com.pmumali.plr.services;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.pmumali.plr.dtos.ChevalDto;
|
||||
import com.pmumali.plr.dtos.ResultatChevalDto;
|
||||
import com.pmumali.plr.dtos.ResultatChevalView;
|
||||
import com.pmumali.plr.dtos.ResultatCreationDto;
|
||||
import com.pmumali.plr.dtos.ResultatDto;
|
||||
import com.pmumali.plr.enums.CourseStatue;
|
||||
import com.pmumali.plr.models.Cheval;
|
||||
import com.pmumali.plr.models.ChevalCourse;
|
||||
import com.pmumali.plr.models.Course;
|
||||
import com.pmumali.plr.models.Resultat;
|
||||
import com.pmumali.plr.models.ResultatCheval;
|
||||
import com.pmumali.plr.repositories.ChevalCourseRepository;
|
||||
import com.pmumali.plr.repositories.ChevalRepository;
|
||||
import com.pmumali.plr.repositories.CourseRepository;
|
||||
import com.pmumali.plr.repositories.ResultatChevalRepository;
|
||||
import com.pmumali.plr.repositories.ResultatRepository;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ResultatService {
|
||||
|
||||
private final ResultatRepository resultatRepository;
|
||||
private final ResultatChevalRepository resultatChevalRepository;
|
||||
private final CourseRepository courseRepository;
|
||||
private final ChevalRepository chevalRepository;
|
||||
private final ChevalCourseRepository chevalCourseRepository;
|
||||
|
||||
// ----------------- CREATE -----------------
|
||||
@Transactional
|
||||
public ResultatDto createResultat(ResultatCreationDto dto) {
|
||||
Course course = courseRepository.findById(dto.getCourseId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Course not found with ID: " + dto.getCourseId()));
|
||||
|
||||
if (resultatRepository.findByCourseId(course.getId()).isPresent()) {
|
||||
throw new IllegalArgumentException("A result already exists for this course.");
|
||||
}
|
||||
|
||||
// Validate horses belong to course & are not NP/DQ
|
||||
validateHorsesForCourseAndStatus(course, dto.getResultatsChevaux());
|
||||
|
||||
Resultat res = new Resultat();
|
||||
res.setCourse(course);
|
||||
res.setDateOfficielle(LocalDateTime.now());
|
||||
if (dto.getNumeroPlusGagnant() != null && !dto.getNumeroPlusGagnant().isBlank()) {
|
||||
res.setNumeroPlusGagnant(dto.getNumeroPlusGagnant());
|
||||
}
|
||||
|
||||
// ⬇️ Save into a new final variable so it’s effectively final for lambdas
|
||||
final Resultat savedRes = resultatRepository.save(res);
|
||||
|
||||
List<ResultatCheval> lignes = dto.getResultatsChevaux().stream()
|
||||
.map(rc -> {
|
||||
Cheval cheval = chevalRepository.findById(rc.getChevalId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Cheval not found: " + rc.getChevalId()));
|
||||
ResultatCheval line = new ResultatCheval();
|
||||
line.setResultat(savedRes); // ⬅️ use savedRes here
|
||||
line.setCheval(cheval);
|
||||
line.setRang(rc.getRang()); // Dead-Heat allowed
|
||||
return line;
|
||||
})
|
||||
.toList();
|
||||
|
||||
resultatChevalRepository.saveAll(lignes);
|
||||
savedRes.setResultatsChevaux(lignes);
|
||||
|
||||
// Close course (official)
|
||||
course.setStatus(CourseStatue.CLOTUREE);
|
||||
courseRepository.save(course);
|
||||
|
||||
return toDto(savedRes);
|
||||
}
|
||||
|
||||
// ----------------- READ -----------------
|
||||
@Transactional(readOnly = true)
|
||||
public List<ResultatDto> findAllResults() {
|
||||
return resultatRepository.findAll().stream().map(this::toDto).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ResultatDto findResultatByCourseId(Long courseId) {
|
||||
return toDto(resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId)));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ResultatChevalView> listResultatChevauxViewExt(Long courseId) {
|
||||
return resultatChevalRepository.findWithChevalAndNumeroByCourseId(courseId).stream()
|
||||
.map(row -> {
|
||||
var rc = (ResultatCheval) row[0];
|
||||
var numero = (Integer) row[1];
|
||||
var ch = rc.getCheval();
|
||||
var chevalDto = new ChevalDto(ch.getId(), ch.getNom(), ch.getNomEcurie(), ch.getBirthYear());
|
||||
return new ResultatChevalView(chevalDto, numero, rc.getRang());
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ----------------- UPDATE (full reorder) -----------------
|
||||
@Transactional
|
||||
public Resultat updateResultat(Long courseId, List<ResultatChevalDto> updated) {
|
||||
if (updated == null || updated.isEmpty()) {
|
||||
throw new IllegalArgumentException("resultatsChevaux is required.");
|
||||
}
|
||||
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
|
||||
// validate (registered, not NP/DQ, ranks > 0)
|
||||
validateHorsesForCourseAndStatus(res.getCourse(), updated);
|
||||
|
||||
// guard: no duplicate chevalId in payload
|
||||
var seen = new java.util.HashSet<Long>();
|
||||
for (var rc : updated) {
|
||||
if (!seen.add(rc.getChevalId())) {
|
||||
throw new IllegalArgumentException("Duplicate chevalId in payload: " + rc.getChevalId());
|
||||
}
|
||||
}
|
||||
|
||||
// 1) delete all existing children for this resultat
|
||||
resultatChevalRepository.deleteByResultat(res);
|
||||
// 2) FLUSH NOW so DB is clean before inserts
|
||||
resultatChevalRepository.flush();
|
||||
|
||||
// 3) reinsert new lines
|
||||
var lines = new java.util.ArrayList<ResultatCheval>(updated.size());
|
||||
for (var dto : updated) {
|
||||
Cheval cheval = chevalRepository.findById(dto.getChevalId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Cheval not found: " + dto.getChevalId()));
|
||||
|
||||
var line = new ResultatCheval();
|
||||
line.setResultat(res);
|
||||
line.setCheval(cheval);
|
||||
line.setRang(dto.getRang()); // Dead-Heat allowed
|
||||
lines.add(line);
|
||||
}
|
||||
resultatChevalRepository.saveAll(lines);
|
||||
|
||||
// keep the managed association in sync (no replace of the list instance)
|
||||
res.getResultatsChevaux().clear();
|
||||
res.getResultatsChevaux().addAll(lines);
|
||||
|
||||
return resultatRepository.save(res);
|
||||
}
|
||||
|
||||
|
||||
// ----------------- DELETE (by course) -----------------
|
||||
@Transactional
|
||||
public void deleteResultat(Long courseId) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
// delete children then parent
|
||||
List<ResultatCheval> lines = resultatChevalRepository.findByResultatOrderByRangAsc(res);
|
||||
resultatChevalRepository.deleteAll(lines);
|
||||
resultatRepository.delete(res);
|
||||
|
||||
// Optionnel: repasser la course en PLANIFIEE ou laissée CLOTUREE (ici on ne change pas)
|
||||
}
|
||||
|
||||
// ----------------- CHILD OPS (ligne résultat) -----------------
|
||||
@Transactional
|
||||
public ResultatChevalView addResultatCheval(Long courseId, ResultatChevalDto dto) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
|
||||
ensureChevalIsValidForCourse(res.getCourse(), dto.getChevalId());
|
||||
|
||||
if (resultatChevalRepository.existsByResultatAndChevalId(res, dto.getChevalId())) {
|
||||
throw new IllegalArgumentException("Cheval already present in this result.");
|
||||
}
|
||||
|
||||
Cheval cheval = chevalRepository.findById(dto.getChevalId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Cheval not found: " + dto.getChevalId()));
|
||||
|
||||
ResultatCheval line = new ResultatCheval();
|
||||
line.setResultat(res);
|
||||
line.setCheval(cheval);
|
||||
line.setRang(dto.getRang()); // Dead-Heat allowed
|
||||
|
||||
line = resultatChevalRepository.save(line);
|
||||
|
||||
// (A) fetch numeroCheval for this horse in this course
|
||||
Integer numero = chevalCourseRepository.findByCourseAndCheval(res.getCourse(), cheval)
|
||||
.map(cc -> cc.getNumeroCheval())
|
||||
.orElse(null);
|
||||
|
||||
ChevalDto chevalDto = new ChevalDto(cheval.getId(), cheval.getNom(), cheval.getNomEcurie(), cheval.getBirthYear());
|
||||
return new ResultatChevalView(chevalDto, numero, line.getRang());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ResultatCheval upsertResultatCheval(Long courseId, ResultatChevalDto dto) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
|
||||
ensureChevalIsValidForCourse(res.getCourse(), dto.getChevalId());
|
||||
|
||||
ResultatCheval line = resultatChevalRepository.findByResultatAndChevalId(res, dto.getChevalId())
|
||||
.orElseGet(() -> {
|
||||
ResultatCheval created = new ResultatCheval();
|
||||
created.setResultat(res);
|
||||
created.setCheval(chevalRepository.findById(dto.getChevalId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Cheval not found: " + dto.getChevalId())));
|
||||
return created;
|
||||
});
|
||||
|
||||
line.setRang(dto.getRang()); // Dead-Heat autorisé
|
||||
return resultatChevalRepository.save(line);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteResultatCheval(Long courseId, Long chevalId) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
resultatChevalRepository.deleteByResultatAndChevalId(res, chevalId);
|
||||
}
|
||||
|
||||
// ----------------- Numero+ (Quinté+) -----------------
|
||||
@Transactional
|
||||
public Resultat setNumeroPlus(Long courseId, String numeroPlus) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
res.setNumeroPlusGagnant(numeroPlus);
|
||||
return resultatRepository.save(res);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Resultat drawNumeroPlus(Long courseId) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
String drawn = String.format("%06d", new java.util.Random().nextInt(1_000_000));
|
||||
res.setNumeroPlusGagnant(drawn);
|
||||
return resultatRepository.save(res);
|
||||
}
|
||||
|
||||
// ----------------- Validation & Helpers règlement -----------------
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> validate(Long courseId) {
|
||||
Resultat res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Result not found for course: " + courseId));
|
||||
List<ResultatCheval> lines = resultatChevalRepository.findByResultatOrderByRangAsc(res);
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
// Tous les chevaux doivent être inscrits & non NP/DQ
|
||||
Set<Long> validChevaux = chevalCourseRepository.findByCourse(res.getCourse()).stream()
|
||||
.filter(cc -> !Boolean.TRUE.equals(cc.getNonPartant()) && !Boolean.TRUE.equals(cc.getEstDisqualifie()))
|
||||
.map(cc -> cc.getCheval().getId()).collect(Collectors.toSet());
|
||||
|
||||
for (ResultatCheval rc : lines) {
|
||||
if (!validChevaux.contains(rc.getCheval().getId())) {
|
||||
errors.add("Cheval " + rc.getCheval().getId() + " not valid (NP/DQ or not registered).");
|
||||
}
|
||||
}
|
||||
|
||||
// Détecter Dead-Heat: rang -> nb chevaux
|
||||
Map<Integer, Long> countsByRank = lines.stream()
|
||||
.collect(Collectors.groupingBy(ResultatCheval::getRang, LinkedHashMap::new, Collectors.counting()));
|
||||
Map<Integer, Long> deadHeats = countsByRank.entrySet().stream()
|
||||
.filter(e -> e.getValue() > 1L)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
boolean ok = errors.isEmpty();
|
||||
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("ok", ok);
|
||||
out.put("errors", errors);
|
||||
out.put("deadHeats", deadHeats); // ex: {1:2} => rang 1 a 2 chevaux ex æquo
|
||||
return out;
|
||||
}
|
||||
|
||||
public ResultatDto toDto(Resultat r) {
|
||||
var lignes = r.getResultatsChevaux() == null ? List.<ResultatDto.LigneDto>of()
|
||||
: r.getResultatsChevaux().stream()
|
||||
.map(rc -> ResultatDto.LigneDto.builder()
|
||||
.chevalId(rc.getCheval().getId())
|
||||
.chevalNom(rc.getCheval().getNom())
|
||||
.rang(rc.getRang())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
return ResultatDto.builder()
|
||||
.id(r.getId())
|
||||
.courseId(r.getCourse().getId())
|
||||
.courseNom(r.getCourse().getNom())
|
||||
.dateOfficielle(r.getDateOfficielle())
|
||||
.numeroPlusGagnant(r.getNumeroPlusGagnant())
|
||||
.arrivee(lignes)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper pour le moteur de paiement:
|
||||
* construit une map rang -> set de chevalId (Dead-Heat friendly).
|
||||
* Exemple: si rang 1 a deux chevaux, map.get(1) = {idA, idB}.
|
||||
*/
|
||||
// @Transactional(readOnly = true)
|
||||
// public Map<Integer, Set<Long>> buildRankToChevalSet(Long courseId) {
|
||||
// Resultat res = findResultatByCourseId(courseId);
|
||||
// List<ResultatCheval> lines = resultatChevalRepository.findByResultatOrderByRangAsc(res);
|
||||
// Map<Integer, Set<Long>> map = new LinkedHashMap<>();
|
||||
// for (ResultatCheval rc : lines) {
|
||||
// map.computeIfAbsent(rc.getRang(), k -> new LinkedHashSet<>()).add(rc.getCheval().getId());
|
||||
// }
|
||||
// return map;
|
||||
// }
|
||||
private Map<Integer, Set<Long>> buildRankToChevalSet(Long courseId) {
|
||||
var res = resultatRepository.findByCourseId(courseId)
|
||||
.orElseThrow(() -> new jakarta.persistence.EntityNotFoundException("Resultat not found for course: " + courseId));
|
||||
|
||||
return resultatChevalRepository.findByResultatOrderByRangAsc(res).stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
rc -> rc.getRang(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.mapping(rc -> rc.getCheval().getId(), Collectors.toCollection(LinkedHashSet::new))
|
||||
));
|
||||
}
|
||||
|
||||
// Renvoie l’ensemble des chevaux occupant les rangs 1..N (avec Dead-Heat)
|
||||
@Transactional(readOnly = true)
|
||||
public Set<Long> topNWithDeadHeat(Long courseId, int n) {
|
||||
Map<Integer, Set<Long>> rankMap = buildRankToChevalSet(courseId);
|
||||
Set<Long> out = new LinkedHashSet<>();
|
||||
for (int i = 1; i <= n; i++) {
|
||||
Set<Long> ids = rankMap.get(i);
|
||||
if (ids != null) out.addAll(ids);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ----------- validations utilitaires internes -----------
|
||||
|
||||
private void validateHorsesForCourseAndStatus(Course course, List<ResultatChevalDto> items) {
|
||||
// Tous doivent être inscrits dans la course
|
||||
Set<Long> courseChevalIds = chevalCourseRepository.findByCourse(course).stream()
|
||||
.map(cc -> cc.getCheval().getId()).collect(Collectors.toSet());
|
||||
|
||||
// Et non NP/DQ
|
||||
Map<Long, ChevalCourse> index = chevalCourseRepository.findByCourse(course).stream()
|
||||
.collect(Collectors.toMap(cc -> cc.getCheval().getId(), cc -> cc));
|
||||
|
||||
for (ResultatChevalDto dto : items) {
|
||||
if (!courseChevalIds.contains(dto.getChevalId())) {
|
||||
throw new IllegalArgumentException("Cheval " + dto.getChevalId() + " not registered in course " + course.getId());
|
||||
}
|
||||
ChevalCourse cc = index.get(dto.getChevalId());
|
||||
if (Boolean.TRUE.equals(cc.getNonPartant())) {
|
||||
throw new IllegalArgumentException("Cheval " + dto.getChevalId() + " is NP in course " + course.getId());
|
||||
}
|
||||
if (Boolean.TRUE.equals(cc.getEstDisqualifie())) {
|
||||
throw new IllegalArgumentException("Cheval " + dto.getChevalId() + " is DQ in course " + course.getId());
|
||||
}
|
||||
if (dto.getRang() == null || dto.getRang() <= 0) {
|
||||
throw new IllegalArgumentException("Invalid rang for cheval " + dto.getChevalId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureChevalIsValidForCourse(Course course, Long chevalId) {
|
||||
ChevalCourse cc = chevalCourseRepository.findByCourseAndChevalId(course, chevalId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Cheval " + chevalId + " not registered in course " + course.getId()));
|
||||
if (Boolean.TRUE.equals(cc.getNonPartant())) {
|
||||
throw new IllegalArgumentException("Cheval " + chevalId + " is NP in course " + course.getId());
|
||||
}
|
||||
if (Boolean.TRUE.equals(cc.getEstDisqualifie())) {
|
||||
throw new IllegalArgumentException("Cheval " + chevalId + " is DQ in course " + course.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,6 @@ spring:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
time_zone: UTC
|
||||
open-in-view: false
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
Reference in New Issue
Block a user