package dev.lions.btpxpress.adapter.http; import dev.lions.btpxpress.application.service.FactureService; import dev.lions.btpxpress.application.service.PdfGeneratorService; import dev.lions.btpxpress.domain.core.entity.Facture; import io.quarkus.security.Authenticated; import jakarta.inject.Inject; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.UUID; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Resource REST pour la gestion des factures - Architecture 2025 MIGRATION: Préservation exacte de * tous les endpoints critiques */ @Path("/api/v1/factures") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = "Factures", description = "Gestion des factures BTP") // @Authenticated - Désactivé pour les tests public class FactureResource { private static final Logger logger = LoggerFactory.getLogger(FactureResource.class); @Inject FactureService factureService; @Inject PdfGeneratorService pdfGeneratorService; // === ENDPOINTS DE CONSULTATION - API CONTRACTS PRÉSERVÉS EXACTEMENT === @GET @Operation(summary = "Récupérer toutes les factures") @APIResponse(responseCode = "200", description = "Liste des factures récupérée avec succès") public Response getAllFactures( @Parameter(description = "Terme de recherche") @QueryParam("search") String search, @Parameter(description = "ID du client") @QueryParam("clientId") String clientId, @Parameter(description = "ID du chantier") @QueryParam("chantierId") String chantierId) { try { List factures; if (clientId != null && !clientId.isEmpty()) { factures = factureService.findByClient(UUID.fromString(clientId)); } else if (chantierId != null && !chantierId.isEmpty()) { factures = factureService.findByChantier(UUID.fromString(chantierId)); } else if (search != null && !search.isEmpty()) { factures = factureService.search(search); } else { factures = factureService.findAll(); } return Response.ok(factures).build(); } catch (Exception e) { logger.error("Erreur lors de la récupération des factures", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la récupération des factures: " + e.getMessage()) .build(); } } @GET @Path("/{id}") @Operation(summary = "Récupérer une facture par ID") @APIResponse(responseCode = "200", description = "Facture récupérée avec succès") @APIResponse(responseCode = "404", description = "Facture non trouvée") public Response getFactureById( @Parameter(description = "ID de la facture") @PathParam("id") String id) { try { UUID factureId = UUID.fromString(id); return factureService .findById(factureId) .map(facture -> Response.ok(facture).build()) .orElse( Response.status(Response.Status.NOT_FOUND) .entity("Facture non trouvée avec l'ID: " + id) .build()); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) .entity("ID de facture invalide: " + id) .build(); } catch (Exception e) { logger.error("Erreur lors de la récupération de la facture {}", id, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la récupération de la facture: " + e.getMessage()) .build(); } } @GET @Path("/count") @Operation(summary = "Compter le nombre de factures") @APIResponse(responseCode = "200", description = "Nombre de factures retourné avec succès") public Response countFactures() { try { long count = factureService.count(); return Response.ok(new CountResponse(count)).build(); } catch (Exception e) { logger.error("Erreur lors du comptage des factures", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors du comptage des factures: " + e.getMessage()) .build(); } } @GET @Path("/stats") @Operation(summary = "Obtenir les statistiques des factures") @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès") public Response getStats() { try { Object stats = factureService.getStatistics(); return Response.ok(stats).build(); } catch (Exception e) { logger.error("Erreur lors de la génération des statistiques des factures", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la génération des statistiques: " + e.getMessage()) .build(); } } @GET @Path("/chiffre-affaires") @Operation(summary = "Calculer le chiffre d'affaires") @APIResponse(responseCode = "200", description = "Chiffre d'affaires calculé avec succès") @APIResponse(responseCode = "400", description = "Format de date invalide") public Response getChiffreAffaires( @Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut") String dateDebut, @Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) { try { BigDecimal chiffre; if (dateDebut != null && dateFin != null) { LocalDate debut = LocalDate.parse(dateDebut); LocalDate fin = LocalDate.parse(dateFin); chiffre = factureService.getChiffreAffairesParPeriode(debut, fin); } else { chiffre = factureService.getChiffreAffaires(); } return Response.ok(new ChiffreAffairesResponse(chiffre)).build(); } catch (Exception e) { logger.error("Erreur lors du calcul du chiffre d'affaires", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors du calcul du chiffre d'affaires: " + e.getMessage()) .build(); } } @GET @Path("/echues") @Operation(summary = "Récupérer les factures échues") @APIResponse( responseCode = "200", description = "Liste des factures échues récupérée avec succès") public Response getFacturesEchues() { try { List factures = factureService.findEchues(); return Response.ok(factures).build(); } catch (Exception e) { logger.error("Erreur lors de la récupération des factures échues", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la récupération des factures échues: " + e.getMessage()) .build(); } } @GET @Path("/proches-echeance") @Operation(summary = "Récupérer les factures proches de l'échéance") @APIResponse( responseCode = "200", description = "Liste des factures proches de l'échéance récupérée avec succès") public Response getFacturesProchesEcheance( @Parameter(description = "Nombre de jours avant l'échéance") @QueryParam("jours") @DefaultValue("7") int jours) { try { List factures = factureService.findProchesEcheance(jours); return Response.ok(factures).build(); } catch (Exception e) { logger.error("Erreur lors de la récupération des factures proches de l'échéance", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity( "Erreur lors de la récupération des factures proches de l'échéance: " + e.getMessage()) .build(); } } // =========================================== // ENDPOINTS DE GESTION // =========================================== @POST @Operation(summary = "Créer une nouvelle facture") @APIResponse(responseCode = "201", description = "Facture créée avec succès") @APIResponse(responseCode = "400", description = "Données invalides") public Response createFacture( @Parameter(description = "Données de la facture à créer") @NotNull CreateFactureRequest request) { try { Facture facture = factureService.create( request.numero, request.clientId, request.chantierId, request.montantHT, request.description); return Response.status(Response.Status.CREATED).entity(facture).build(); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) .entity("Données invalides: " + e.getMessage()) .build(); } catch (Exception e) { logger.error("Erreur lors de la création de la facture", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la création de la facture: " + e.getMessage()) .build(); } } @PUT @Path("/{id}") @Operation(summary = "Mettre à jour une facture") @APIResponse(responseCode = "200", description = "Facture mise à jour avec succès") @APIResponse(responseCode = "400", description = "Données invalides") @APIResponse(responseCode = "404", description = "Facture non trouvée") public Response updateFacture( @Parameter(description = "ID de la facture") @PathParam("id") String id, @Parameter(description = "Données de mise à jour de la facture") @NotNull UpdateFactureRequest request) { try { UUID factureId = UUID.fromString(id); Facture facture = factureService.update( factureId, request.description, request.montantHT, request.dateEcheance); return Response.ok(facture).build(); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) .entity("Données invalides: " + e.getMessage()) .build(); } catch (Exception e) { logger.error("Erreur lors de la mise à jour de la facture {}", id, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la mise à jour de la facture: " + e.getMessage()) .build(); } } @DELETE @Path("/{id}") @Operation(summary = "Supprimer une facture") @APIResponse(responseCode = "204", description = "Facture supprimée avec succès") @APIResponse(responseCode = "400", description = "ID invalide") @APIResponse(responseCode = "404", description = "Facture non trouvée") public Response deleteFacture( @Parameter(description = "ID de la facture") @PathParam("id") String id) { try { UUID factureId = UUID.fromString(id); factureService.delete(factureId); return Response.noContent().build(); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) .entity("ID invalide: " + e.getMessage()) .build(); } catch (Exception e) { logger.error("Erreur lors de la suppression de la facture {}", id, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la suppression de la facture: " + e.getMessage()) .build(); } } // =========================================== // ENDPOINTS DE RECHERCHE AVANCÉE // =========================================== @GET @Path("/date-range") @Operation(summary = "Récupérer les factures par plage de dates") @APIResponse(responseCode = "200", description = "Liste des factures récupérée avec succès") @APIResponse(responseCode = "400", description = "Paramètres de date invalides") public Response getFacturesByDateRange( @Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut") String dateDebut, @Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) { try { if (dateDebut == null || dateFin == null) { return Response.status(Response.Status.BAD_REQUEST) .entity("Les paramètres dateDebut et dateFin sont obligatoires") .build(); } LocalDate debut = LocalDate.parse(dateDebut); LocalDate fin = LocalDate.parse(dateFin); if (debut.isAfter(fin)) { return Response.status(Response.Status.BAD_REQUEST) .entity("La date de début ne peut pas être après la date de fin") .build(); } List factures = factureService.findByDateRange(debut, fin); return Response.ok(factures).build(); } catch (Exception e) { logger.error("Erreur lors de la recherche par plage de dates", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la recherche: " + e.getMessage()) .build(); } } @GET @Path("/generate-numero") @Operation(summary = "Générer un numéro de facture") @APIResponse(responseCode = "200", description = "Numéro généré avec succès") public Response generateNumero() { try { String numero = factureService.generateNextNumero(); return Response.ok(new NumeroResponse(numero)).build(); } catch (Exception e) { logger.error("Erreur lors de la génération du numéro de facture", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur lors de la génération du numéro: " + e.getMessage()) .build(); } } // =========================================== // CLASSES UTILITAIRES // =========================================== public static record CountResponse(long count) {} public static record ChiffreAffairesResponse(BigDecimal montant) {} public static record NumeroResponse(String numero) {} public static record CreateFactureRequest( String numero, UUID clientId, UUID chantierId, BigDecimal montantHT, String description) {} public static record UpdateFactureRequest( String description, BigDecimal montantHT, LocalDate dateEcheance) {} // === ENDPOINTS WORKFLOW ET STATUTS === @PUT @Path("/{id}/statut") @Operation(summary = "Mettre à jour le statut d'une facture") @APIResponse(responseCode = "200", description = "Statut mis à jour avec succès") @APIResponse(responseCode = "404", description = "Facture non trouvée") @APIResponse(responseCode = "400", description = "Transition de statut invalide") public Response updateFactureStatut( @Parameter(description = "ID de la facture") @PathParam("id") UUID id, @Parameter(description = "Nouveau statut") @QueryParam("statut") @NotNull Facture.StatutFacture statut) { logger.debug("PUT /factures/{}/statut - nouveau statut: {}", id, statut); Facture updatedFacture = factureService.updateStatut(id, statut); return Response.ok(updatedFacture).build(); } @PUT @Path("/{id}/payer") @Operation(summary = "Marquer une facture comme payée") @APIResponse(responseCode = "200", description = "Facture marquée comme payée") @APIResponse(responseCode = "404", description = "Facture non trouvée") @APIResponse(responseCode = "400", description = "Facture ne peut pas être marquée comme payée") public Response marquerFacturePayee( @Parameter(description = "ID de la facture") @PathParam("id") UUID id) { logger.debug("PUT /factures/{}/payer", id); Facture facturePayee = factureService.marquerPayee(id); return Response.ok(facturePayee).build(); } // === ENDPOINTS CONVERSION DEVIS === @POST @Path("/from-devis/{devisId}") @Operation(summary = "Créer une facture à partir d'un devis") @APIResponse(responseCode = "201", description = "Facture créée à partir du devis") @APIResponse(responseCode = "404", description = "Devis non trouvé") @APIResponse(responseCode = "400", description = "Devis ne peut pas être converti") public Response createFactureFromDevis( @Parameter(description = "ID du devis") @PathParam("devisId") UUID devisId) { logger.debug("POST /factures/from-devis/{}", devisId); Facture facture = factureService.createFromDevis(devisId); return Response.status(Response.Status.CREATED).entity(facture).build(); } // === ENDPOINTS RECHERCHE PAR STATUT === @GET @Path("/statut/{statut}") @Operation(summary = "Récupérer les factures par statut") @APIResponse(responseCode = "200", description = "Factures par statut récupérées") public Response getFacturesByStatut( @Parameter(description = "Statut des factures") @PathParam("statut") Facture.StatutFacture statut) { logger.debug("GET /factures/statut/{}", statut); List factures = factureService.findByStatut(statut); return Response.ok(factures).build(); } @GET @Path("/brouillons") @Operation(summary = "Récupérer les factures brouillons") @APIResponse(responseCode = "200", description = "Factures brouillons récupérées") public Response getFacturesBrouillons() { logger.debug("GET /factures/brouillons"); List factures = factureService.findBrouillons(); return Response.ok(factures).build(); } @GET @Path("/envoyees") @Operation(summary = "Récupérer les factures envoyées") @APIResponse(responseCode = "200", description = "Factures envoyées récupérées") public Response getFacturesEnvoyees() { logger.debug("GET /factures/envoyees"); List factures = factureService.findEnvoyees(); return Response.ok(factures).build(); } @GET @Path("/payees") @Operation(summary = "Récupérer les factures payées") @APIResponse(responseCode = "200", description = "Factures payées récupérées") public Response getFacturesPayees() { logger.debug("GET /factures/payees"); List factures = factureService.findPayees(); return Response.ok(factures).build(); } @GET @Path("/en-retard") @Operation(summary = "Récupérer les factures en retard") @APIResponse(responseCode = "200", description = "Factures en retard récupérées") public Response getFacturesEnRetard() { logger.debug("GET /factures/en-retard"); List factures = factureService.findEnRetard(); return Response.ok(factures).build(); } // === ENDPOINTS PDF === @GET @Path("/{id}/pdf") @Operation(summary = "Générer le PDF d'une facture") @APIResponse(responseCode = "200", description = "PDF généré avec succès") @APIResponse(responseCode = "404", description = "Facture non trouvée") public Response generateFacturePdf( @Parameter(description = "ID de la facture") @PathParam("id") UUID id) { logger.debug("GET /factures/{}/pdf", id); Facture facture = factureService .findById(id) .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Facture non trouvée")); byte[] pdfContent = pdfGeneratorService.generateFacturePdf(facture); String fileName = pdfGeneratorService.generateFileName("facture", facture.getNumero()); return Response.ok(pdfContent) .header("Content-Type", "application/pdf") .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"") .build(); } }