package com.lions.dev.resource; import com.lions.dev.dto.request.promotion.PromotionCreateRequestDTO; import com.lions.dev.dto.request.promotion.PromotionUpdateRequestDTO; import com.lions.dev.dto.response.promotion.PromotionResponseDTO; import com.lions.dev.entity.promotion.Promotion; import com.lions.dev.security.Permission; import com.lions.dev.security.RequiresPermission; import com.lions.dev.service.PromotionService; import com.lions.dev.service.SecurityService; import com.lions.dev.util.UserRoles; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; /** * Ressource REST pour la gestion des promotions dans le système AfterWork. * * SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification. * Seul le responsable de l'établissement peut créer/modifier/supprimer des promotions. * * @since 2.0 - Sécurité JWT + RBAC production-ready */ @Path("/promotions") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = "Promotions", description = "Opérations liées à la gestion des promotions") public class PromotionResource { @Inject PromotionService promotionService; @Inject SecurityService securityService; private static final Logger LOG = Logger.getLogger(PromotionResource.class); // ===================================================================== // ENDPOINTS PUBLICS (LECTURE) // ===================================================================== /** * Récupère toutes les promotions actives et valides. * * @param page Le numéro de la page (0-indexé) * @param size La taille de la page * @return Liste paginée des promotions actives */ @GET @PermitAll @Operation( summary = "Récupérer toutes les promotions actives", description = "Retourne une liste paginée de toutes les promotions actives et valides") @APIResponse(responseCode = "200", description = "Liste des promotions") public Response getAllActivePromotions( @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("10") int size) { LOG.info("[LOG] Récupération de toutes les promotions actives (page: " + page + ", size: " + size + ")"); try { List promotions = promotionService.getAllActivePromotions(page, size); List responseDTOs = promotions.stream() .map(PromotionResponseDTO::new) .collect(Collectors.toList()); return Response.ok(responseDTOs).build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la récupération des promotions : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la récupération des promotions.\"}") .build(); } } /** * Récupère une promotion par son ID. * * @param promotionId L'ID de la promotion * @return La promotion trouvée */ @GET @Path("/{id}") @PermitAll @Operation( summary = "Récupérer une promotion par ID", description = "Retourne les détails d'une promotion spécifique") @APIResponse(responseCode = "200", description = "Promotion trouvée") @APIResponse(responseCode = "404", description = "Promotion non trouvée") public Response getPromotionById(@PathParam("id") UUID promotionId) { LOG.info("[LOG] Récupération de la promotion ID : " + promotionId); try { Promotion promotion = promotionService.getPromotionById(promotionId); PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion); return Response.ok(responseDTO).build(); } catch (IllegalArgumentException e) { LOG.warn("[WARN] Promotion non trouvée : " + e.getMessage()); return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"Promotion non trouvée.\"}") .build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la récupération de la promotion : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la récupération de la promotion.\"}") .build(); } } /** * Recherche une promotion par code promo. * * @param code Le code promo à rechercher * @return La promotion trouvée */ @GET @Path("/code/{code}") @PermitAll @Operation( summary = "Rechercher une promotion par code promo", description = "Retourne la promotion correspondant au code promo") @APIResponse(responseCode = "200", description = "Promotion trouvée") @APIResponse(responseCode = "404", description = "Code promo non trouvé") public Response getPromotionByCode(@PathParam("code") String code) { LOG.info("[LOG] Recherche de la promotion avec le code : " + code); try { Optional promotionOpt = promotionService.getPromotionByCode(code); if (promotionOpt.isEmpty()) { return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"Code promo non trouvé.\"}") .build(); } PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotionOpt.get()); return Response.ok(responseDTO).build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la recherche du code promo : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la recherche du code promo.\"}") .build(); } } /** * Récupère les promotions d'un établissement. * * @param establishmentId L'ID de l'établissement * @param activeOnly Si true, retourne uniquement les promotions actives et valides * @param page Le numéro de la page * @param size La taille de la page * @return Liste des promotions */ @GET @Path("/establishment/{establishmentId}") @PermitAll @Operation( summary = "Récupérer les promotions d'un établissement", description = "Retourne les promotions d'un établissement spécifique") @APIResponse(responseCode = "200", description = "Liste des promotions") public Response getPromotionsByEstablishment( @PathParam("establishmentId") UUID establishmentId, @QueryParam("activeOnly") @DefaultValue("false") boolean activeOnly, @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("10") int size) { LOG.info("[LOG] Récupération des promotions pour l'établissement : " + establishmentId); try { List promotions; if (activeOnly) { promotions = promotionService.getActivePromotionsByEstablishment(establishmentId); } else { promotions = promotionService.getPromotionsByEstablishment(establishmentId, page, size); } List responseDTOs = promotions.stream() .map(PromotionResponseDTO::new) .collect(Collectors.toList()); return Response.ok(responseDTOs).build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la récupération des promotions : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la récupération des promotions.\"}") .build(); } } // ===================================================================== // ENDPOINTS PROTÉGÉS (CRÉATION, MODIFICATION, SUPPRESSION) // ===================================================================== /** * Crée une nouvelle promotion. * Requiert une authentification JWT. * Seul le responsable de l'établissement peut créer des promotions. * * @param requestContext Le contexte de la requête (injecté) * @param requestDTO Le DTO de création * @return La promotion créée */ @POST @Transactional @RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN}) @RequiresPermission(Permission.PROMOTIONS_CREATE) @Operation( summary = "Créer une promotion", description = "Crée une nouvelle promotion pour un établissement. Seul le responsable peut créer.") @SecurityRequirement(name = "bearerAuth") @APIResponse(responseCode = "201", description = "Promotion créée avec succès") @APIResponse(responseCode = "400", description = "Données invalides") @APIResponse(responseCode = "401", description = "Non authentifié") @APIResponse(responseCode = "403", description = "Non autorisé à créer des promotions pour cet établissement") public Response createPromotion(@Valid PromotionCreateRequestDTO requestDTO) { UUID authenticatedUserId = securityService.getCurrentUserId(); LOG.info("[LOG] Création d'une promotion pour l'établissement : " + requestDTO.getEstablishmentId() + " par l'utilisateur : " + authenticatedUserId); try { Promotion promotion = promotionService.createPromotion(requestDTO); // Vérifier que l'utilisateur est bien le responsable if (!promotionService.canModifyPromotion(promotion, authenticatedUserId)) { promotionService.deletePromotion(promotion.getId()); // Rollback LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à créer des promotions pour cet établissement"); return Response.status(Response.Status.FORBIDDEN) .entity("{\"message\": \"Vous n'êtes pas autorisé à créer des promotions pour cet établissement.\"}") .build(); } PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion); return Response.status(Response.Status.CREATED).entity(responseDTO).build(); } catch (IllegalArgumentException e) { LOG.warn("[WARN] " + e.getMessage()); return Response.status(Response.Status.BAD_REQUEST) .entity("{\"message\": \"" + e.getMessage() + "\"}") .build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la création de la promotion : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la création de la promotion.\"}") .build(); } } /** * Met à jour une promotion. * Requiert une authentification JWT. * Seul le responsable de l'établissement peut modifier. * * @param requestContext Le contexte de la requête (injecté) * @param promotionId L'ID de la promotion * @param requestDTO Le DTO de mise à jour * @return La promotion mise à jour */ @PUT @Path("/{id}") @Transactional @RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN}) @RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN) @Operation( summary = "Mettre à jour une promotion", description = "Met à jour une promotion existante. Seul le responsable peut modifier.") @SecurityRequirement(name = "bearerAuth") @APIResponse(responseCode = "200", description = "Promotion mise à jour") @APIResponse(responseCode = "401", description = "Non authentifié") @APIResponse(responseCode = "403", description = "Non autorisé à modifier cette promotion") @APIResponse(responseCode = "404", description = "Promotion non trouvée") public Response updatePromotion( @PathParam("id") UUID promotionId, @Valid PromotionUpdateRequestDTO requestDTO) { UUID authenticatedUserId = securityService.getCurrentUserId(); LOG.info("[LOG] Mise à jour de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId); try { // Vérifier les permissions Promotion existingPromotion = promotionService.getPromotionById(promotionId); if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) { LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à modifier la promotion " + promotionId); return Response.status(Response.Status.FORBIDDEN) .entity("{\"message\": \"Vous n'êtes pas autorisé à modifier cette promotion.\"}") .build(); } Promotion promotion = promotionService.updatePromotion(promotionId, requestDTO); PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion); return Response.ok(responseDTO).build(); } catch (IllegalArgumentException e) { LOG.warn("[WARN] " + e.getMessage()); return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"" + e.getMessage() + "\"}") .build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la mise à jour de la promotion : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la mise à jour de la promotion.\"}") .build(); } } /** * Supprime une promotion. * Requiert une authentification JWT. * Seul le responsable de l'établissement peut supprimer. * * @param requestContext Le contexte de la requête (injecté) * @param promotionId L'ID de la promotion * @return Confirmation de suppression */ @DELETE @Path("/{id}") @Transactional @RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN}) @RequiresPermission(Permission.PROMOTIONS_DELETE_OWN) @Operation( summary = "Supprimer une promotion", description = "Supprime une promotion. Seul le responsable peut supprimer.") @SecurityRequirement(name = "bearerAuth") @APIResponse(responseCode = "204", description = "Promotion supprimée") @APIResponse(responseCode = "401", description = "Non authentifié") @APIResponse(responseCode = "403", description = "Non autorisé à supprimer cette promotion") @APIResponse(responseCode = "404", description = "Promotion non trouvée") public Response deletePromotion(@PathParam("id") UUID promotionId) { UUID authenticatedUserId = securityService.getCurrentUserId(); LOG.info("[LOG] Suppression de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId); try { // Vérifier les permissions Promotion existingPromotion = promotionService.getPromotionById(promotionId); if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) { LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à supprimer la promotion " + promotionId); return Response.status(Response.Status.FORBIDDEN) .entity("{\"message\": \"Vous n'êtes pas autorisé à supprimer cette promotion.\"}") .build(); } boolean deleted = promotionService.deletePromotion(promotionId); if (deleted) { return Response.noContent().build(); } else { return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"Promotion non trouvée.\"}") .build(); } } catch (IllegalArgumentException e) { LOG.warn("[WARN] " + e.getMessage()); return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"Promotion non trouvée.\"}") .build(); } catch (Exception e) { LOG.error("[ERROR] Erreur lors de la suppression de la promotion : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la suppression de la promotion.\"}") .build(); } } /** * Active ou désactive une promotion. * Requiert une authentification JWT. * * @param requestContext Le contexte de la requête (injecté) * @param promotionId L'ID de la promotion * @param isActive L'état à appliquer * @return La promotion mise à jour */ @PATCH @Path("/{id}/active") @Transactional @RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN}) @RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN) @Operation( summary = "Activer/Désactiver une promotion", description = "Change l'état actif d'une promotion. Seul le responsable peut modifier.") @SecurityRequirement(name = "bearerAuth") @APIResponse(responseCode = "200", description = "État mis à jour") @APIResponse(responseCode = "401", description = "Non authentifié") @APIResponse(responseCode = "403", description = "Non autorisé") @APIResponse(responseCode = "404", description = "Promotion non trouvée") public Response setPromotionActive( @PathParam("id") UUID promotionId, @QueryParam("active") @DefaultValue("true") boolean isActive) { UUID authenticatedUserId = securityService.getCurrentUserId(); LOG.info("[LOG] Changement d'état de la promotion " + promotionId + " à " + isActive); try { // Vérifier les permissions Promotion existingPromotion = promotionService.getPromotionById(promotionId); if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) { LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé"); return Response.status(Response.Status.FORBIDDEN) .entity("{\"message\": \"Vous n'êtes pas autorisé à modifier cette promotion.\"}") .build(); } Promotion promotion = promotionService.setPromotionActive(promotionId, isActive); PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion); return Response.ok(responseDTO).build(); } catch (IllegalArgumentException e) { LOG.warn("[WARN] " + e.getMessage()); return Response.status(Response.Status.NOT_FOUND) .entity("{\"message\": \"Promotion non trouvée.\"}") .build(); } catch (Exception e) { LOG.error("[ERROR] Erreur : " + e.getMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"message\": \"Erreur lors de la mise à jour.\"}") .build(); } } }