PosController.java
package com.archiweb.api;
import com.archiweb.model.Code;
import com.archiweb.model.Participation;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.CodeRepository;
import com.archiweb.repository.ParticipationRepository;
import com.archiweb.repository.WinRepository;
import com.archiweb.service.TraceService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Optional;
@RestController
@RequestMapping("/api/pos")
@Tag(
name = "Espace Employés (POS)",
description = """
Module dédié aux opérations réalisées en boutique (Point of Sale).
Ce contrôleur permet :
- La **vérification d’un code scanné** par un employé avant remise du lot.
- La **validation de la remise** d’un lot après confirmation du code.
Toutes les actions nécessitent un compte disposant du rôle **EMPLOYEE**.
"""
)
public class PosController {
private static final Logger log = LoggerFactory.getLogger(PosController.class);
private final CodeRepository codeRepository;
private final WinRepository winRepository;
private final ParticipationRepository participationRepository;
private final TraceService traceService;
public PosController(CodeRepository codeRepository, WinRepository winRepository,
ParticipationRepository participationRepository, TraceService traceService) {
this.codeRepository = codeRepository;
this.winRepository = winRepository;
this.participationRepository = participationRepository;
this.traceService = traceService;
}
// =========================================================
// Vérification d’un code scanné
// =========================================================
@Operation(
summary = "Vérifier un code scanné",
description = """
Permet à un employé connecté de vérifier la validité d'un code client avant la remise du lot.
Le code est considéré comme valide s'il :
- Existe dans la base.
- N'est pas expiré (`expiresAt` non dépassé).
- A été utilisé par un client pour participer (`used = true`).
Note : Un code doit être utilisé pour participer avant qu'un employé puisse le scanner pour valider la remise.
""",
responses = {
@ApiResponse(
responseCode = "200",
description = "Code valide et disponible",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"id": 1523,
"valeur": "CODE-ABCD1234",
"prizeType": "COFFRET_39",
"used": false,
"expiresAt": "2025-12-31T23:59:59"
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "Code invalide, expiré ou déjà utilisé",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = "{ \"error\": \"Code expiré ou déjà utilisé.\" }"
)
)
),
@ApiResponse(
responseCode = "403",
description = "Accès refusé : rôle EMPLOYEE requis",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Access denied. EMPLOYEE role required.\" }")
)
)
}
)
// @PreAuthorize("hasRole('EMPLOYEE')") // Temporairement désactivé pour permettre l'accès public
@PostMapping("/scan")
public ResponseEntity<?> scanCode(@RequestParam String valeur) {
log.info("Scan de code par employé: {}", valeur);
Optional<Code> codeOpt = codeRepository.findByValeur(valeur);
if (codeOpt.isEmpty()) {
log.warn("Code introuvable: {}", valeur);
return ResponseEntity.badRequest().body("Code invalide.");
}
Code code = codeOpt.get();
log.debug("Code trouvé: id={}, used={}, prizeType={}", code.getId(), code.isUsed(), code.getPrizeType());
// Vérifier si le code est expiré
if (code.getExpiresAt() != null && code.getExpiresAt().isBefore(LocalDateTime.now())) {
log.warn("Code expiré: {}", valeur);
return ResponseEntity.badRequest().body("Code expiré.");
}
// Le code doit avoir été utilisé pour participer (used = true)
// pour qu'un employé puisse le scanner et valider la remise
if (!code.isUsed()) {
log.info("Code non encore utilisé pour participer: {}", valeur);
return ResponseEntity.badRequest().body("Ce code n'a pas encore été utilisé pour participer.");
}
// Trouver la participation associée à ce code
List<Participation> participations = participationRepository.findByCode(code);
log.debug("Participations trouvées pour le code: {}", participations.size());
if (participations.isEmpty()) {
log.warn("Aucune participation trouvée pour le code: {}", valeur);
return ResponseEntity.badRequest().body("Aucune participation trouvée pour ce code.");
}
// Prendre la première participation (normalement il n'y en a qu'une par code)
Participation participation = participations.get(0);
User user = participation.getUser();
if (user == null) {
log.error("Utilisateur null dans la participation pour le code: {}", valeur);
return ResponseEntity.badRequest().body("Utilisateur introuvable pour ce code.");
}
log.debug("Utilisateur trouvé: id={}, email={}", user.getId(), user.getEmail());
// Trouver le gain de cet utilisateur avec le prizeType du code
// On cherche d'abord un gain non réclamé, sinon on prend le dernier gain (même s'il est réclamé)
List<Win> userWins = winRepository.findByUserId(user.getId());
log.debug("Gains trouvés pour l'utilisateur {}: {}", user.getId(), userWins.size());
// Chercher le gain correspondant au code
// Le prizeName dans Win doit correspondre au prizeType du code
String expectedPrizeName = code.getPrizeType().name();
log.debug("Recherche d'un gain avec prizeName: {}", expectedPrizeName);
Optional<Win> winOpt = userWins.stream()
.filter(win -> {
boolean matches = win.getPrizeName() != null && win.getPrizeName().equals(expectedPrizeName);
log.debug("Gain id={}, prizeName={}, matches={}", win.getId(), win.getPrizeName(), matches);
return matches;
})
.sorted((w1, w2) -> {
// Prioriser les gains non réclamés, puis trier par date de création (plus récent en premier)
if (w1.isClaimed() != w2.isClaimed()) {
return w1.isClaimed() ? 1 : -1; // Non réclamé en premier
}
if (w1.getCreatedAt() != null && w2.getCreatedAt() != null) {
return w2.getCreatedAt().compareTo(w1.getCreatedAt()); // Plus récent en premier
}
return 0;
})
.findFirst();
if (winOpt.isEmpty()) {
log.warn("Aucun gain trouvé pour l'utilisateur {} avec prizeType {}", user.getId(), code.getPrizeType().name());
log.debug("Tous les gains de l'utilisateur: {}", userWins.stream().map(w -> w.getPrizeName() + " (claimed: " + w.isClaimed() + ")").toList());
return ResponseEntity.badRequest().body("Aucun gain trouvé pour ce code. Le code a peut-être été utilisé mais aucun gain n'a été créé.");
}
Win win = winOpt.get();
log.info("Gain trouvé: id={}, prizeName={}, claimed={}", win.getId(), win.getPrizeName(), win.isClaimed());
// Construire la réponse avec les informations du code, du gain et de l'utilisateur
Map<String, Object> response = new HashMap<>();
Map<String, Object> codeMap = new HashMap<>();
codeMap.put("id", code.getId());
codeMap.put("valeur", code.getValeur());
codeMap.put("prizeType", code.getPrizeType() != null ? code.getPrizeType().name() : null);
codeMap.put("used", code.isUsed());
response.put("code", codeMap);
Map<String, Object> winMap = new HashMap<>();
winMap.put("id", win.getId());
winMap.put("prizeName", win.getPrizeName());
winMap.put("claimed", win.isClaimed());
winMap.put("createdAt", win.getCreatedAt() != null ? win.getCreatedAt().toString() : null);
response.put("win", winMap);
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("email", user.getEmail());
userMap.put("firstName", user.getFirstName() != null ? user.getFirstName() : "");
userMap.put("lastName", user.getLastName() != null ? user.getLastName() : "");
userMap.put("phone", user.getPhone() != null ? user.getPhone() : "");
response.put("user", userMap);
return ResponseEntity.ok(response);
}
// =========================================================
// 2️⃣ Validation de la remise d’un lot
// =========================================================
@Operation(
summary = "Valider la remise d’un lot",
description = """
Permet à un employé connecté de **marquer un code comme remis** après vérification en boutique.
L’opération est enregistrée dans le système de traçabilité interne pour audit (via `TraceService`).
""",
responses = {
@ApiResponse(
responseCode = "200",
description = "Code marqué comme remis avec succès",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"status": "success",
"message": "Code marqué comme remis avec succès.",
"code": "CODE-ABCD1234",
"employee": "employee@example.com",
"timestamp": "2025-10-27T14:05:23"
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "Code invalide ou déjà remis",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = "{ \"error\": \"Ce code a déjà été remis.\" }"
)
)
),
@ApiResponse(
responseCode = "403",
description = "Accès refusé : rôle EMPLOYEE requis",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Access denied. EMPLOYEE role required.\" }")
)
)
}
)
// @PreAuthorize("hasRole('EMPLOYEE')") // Temporairement désactivé pour permettre l'accès public
@PostMapping("/claim")
public ResponseEntity<?> claimCode(@RequestParam String valeur, @RequestParam String employeeEmail) {
Map<String, Object> errorResponse = new HashMap<>();
Optional<Code> codeOpt = codeRepository.findByValeur(valeur);
if (codeOpt.isEmpty()) {
errorResponse.put("status", "error");
errorResponse.put("message", "Code introuvable.");
return ResponseEntity.badRequest().body(errorResponse);
}
Code code = codeOpt.get();
// Le code doit avoir été utilisé pour participer
if (!code.isUsed()) {
errorResponse.put("status", "error");
errorResponse.put("message", "Ce code n'a pas encore été utilisé pour participer.");
return ResponseEntity.badRequest().body(errorResponse);
}
// Trouver le gain correspondant qui n'est pas encore réclamé
Optional<Win> winOpt = winRepository.findFirstByPrizeNameAndClaimedFalse(code.getPrizeType().name());
if (winOpt.isEmpty()) {
errorResponse.put("status", "error");
errorResponse.put("message", "Aucun gain disponible pour ce code ou tous les gains ont déjà été réclamés.");
return ResponseEntity.badRequest().body(errorResponse);
}
Win win = winOpt.get();
// Vérifier si le gain est déjà réclamé (double vérification)
if (win.isClaimed()) {
errorResponse.put("status", "error");
errorResponse.put("message", "Ce gain a déjà été remis.");
return ResponseEntity.badRequest().body(errorResponse);
}
// Marquer le gain comme réclamé (pas le code, le gain !)
win.setClaimed(true);
win.setClaimedAt(LocalDateTime.now());
winRepository.save(win);
traceService.traceRemise(employeeEmail, valeur);
// Retourner une réponse JSON pour éviter les erreurs de parsing
Map<String, Object> response = new HashMap<>();
response.put("status", "success");
response.put("message", "Code marqué comme remis avec succès.");
response.put("code", valeur);
response.put("employee", employeeEmail);
response.put("timestamp", LocalDateTime.now().toString());
return ResponseEntity.ok(response);
}
}