fix(security): audit RBAC complet v3.0 — rôles normalisés, lifecycle, changement mdp mobile
RBAC:
- HealthResource: @PermitAll
- RoleResource: @RolesAllowed ADMIN/SUPER_ADMIN/ADMIN_ORGANISATION class-level
- PropositionAideResource: @RolesAllowed MEMBRE/USER class-level
- AuthCallbackResource: @PermitAll
- EvenementResource: @PermitAll /publics et /test, count restreint
- BackupResource/LogsMonitoringResource/SystemResource: MODERATOR → MODERATEUR
- AnalyticsResource: MANAGER/MEMBER → ADMIN_ORGANISATION/MEMBRE
- RoleConstant.java: constantes de rôles centralisées
Cycle de vie membres:
- MemberLifecycleService: ajouterMembre()/retirerMembre() sur activation/radiation/archivage
- MembreResource: endpoint GET /numero/{numeroMembre}
- MembreService: méthode trouverParNumeroMembre()
Changement mot de passe:
- CompteAdherentResource: endpoint POST /auth/change-password (mobile)
- MembreKeycloakSyncService: changerMotDePasseDirectKeycloak() via API Admin Keycloak directe
- Fallback automatique si lions-user-manager indisponible
Workflow:
- Flyway V17-V23: rôles, types org, formules Option C, lifecycle columns, bareme cotisation
- Nouvelles classes: MemberLifecycleService, OrganisationModuleService, scheduler
- Security: OrganisationContextFilter, OrganisationContextHolder, ModuleAccessFilter
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package de.lions.unionflow.server.auth;
|
||||
|
||||
import io.quarkus.security.PermitAll;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
@@ -11,6 +12,7 @@ import org.jboss.logging.Logger;
|
||||
* mobile.
|
||||
*/
|
||||
@Path("/auth")
|
||||
@PermitAll
|
||||
public class AuthCallbackResource {
|
||||
|
||||
private static final Logger log = Logger.getLogger(AuthCallbackResource.class);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Barème de cotisation par rôle fonctionnel au sein d'une organisation.
|
||||
*
|
||||
* <p>Permet de définir des montants différenciés selon le rôle du membre
|
||||
* (PRESIDENT, TRESORIER, MEMBRE_ORDINAIRE, etc.).
|
||||
*
|
||||
* <p>Si aucun barème n'est défini pour un rôle donné, le système utilise
|
||||
* le montant par défaut de {@link ParametresCotisationOrganisation}.
|
||||
*
|
||||
* <p>Table : {@code bareme_cotisation_role}
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "bareme_cotisation_role",
|
||||
uniqueConstraints = @UniqueConstraint(
|
||||
name = "uq_bareme_cot_org_role",
|
||||
columnNames = {"organisation_id", "role_org"}
|
||||
)
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BaremeCotisationRole extends BaseEntity {
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "organisation_id", nullable = false)
|
||||
private Organisation organisation;
|
||||
|
||||
/**
|
||||
* Rôle fonctionnel dans l'organisation (ex: PRESIDENT, TRESORIER, SECRETAIRE, MEMBRE_ORDINAIRE).
|
||||
* Correspond à {@link dev.lions.unionflow.server.entity.MembreOrganisation#getRoleOrg()}.
|
||||
*/
|
||||
@NotBlank
|
||||
@Column(name = "role_org", nullable = false, length = 50)
|
||||
private String roleOrg;
|
||||
|
||||
@Builder.Default
|
||||
@DecimalMin("0.00")
|
||||
@Digits(integer = 10, fraction = 2)
|
||||
@Column(name = "montant_mensuel", nullable = false, precision = 12, scale = 2)
|
||||
private BigDecimal montantMensuel = BigDecimal.ZERO;
|
||||
|
||||
@Builder.Default
|
||||
@DecimalMin("0.00")
|
||||
@Digits(integer = 10, fraction = 2)
|
||||
@Column(name = "montant_annuel", nullable = false, precision = 12, scale = 2)
|
||||
private BigDecimal montantAnnuel = BigDecimal.ZERO;
|
||||
|
||||
/** Description optionnelle du barème (ex: "Taux réduit bureau exécutif"). */
|
||||
@Column(name = "description", length = 255)
|
||||
private String description;
|
||||
}
|
||||
@@ -73,6 +73,15 @@ public class ParametresCotisationOrganisation extends BaseEntity {
|
||||
@Column(name = "cotisation_obligatoire", nullable = false)
|
||||
private Boolean cotisationObligatoire = true;
|
||||
|
||||
/**
|
||||
* Active la génération automatique mensuelle des cotisations pour cette organisation.
|
||||
* Quand {@code true}, un job planifié crée automatiquement une cotisation par membre actif
|
||||
* le 1er de chaque mois, en utilisant les barèmes par rôle ou le montant par défaut.
|
||||
*/
|
||||
@Builder.Default
|
||||
@Column(name = "generation_automatique_activee", nullable = false)
|
||||
private Boolean generationAutomatiqueActivee = false;
|
||||
|
||||
// ── Méthodes métier ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.BaremeCotisationRole;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class BaremeCotisationRoleRepository implements PanacheRepositoryBase<BaremeCotisationRole, UUID> {
|
||||
|
||||
public Optional<BaremeCotisationRole> findByOrganisationIdAndRoleOrg(UUID organisationId, String roleOrg) {
|
||||
return find("organisation.id = ?1 AND roleOrg = ?2", organisationId, roleOrg).firstResultOptional();
|
||||
}
|
||||
|
||||
public List<BaremeCotisationRole> findByOrganisationId(UUID organisationId) {
|
||||
return find("organisation.id = ?1", organisationId).list();
|
||||
}
|
||||
}
|
||||
@@ -601,4 +601,22 @@ public class CotisationRepository extends BaseRepository<Cotisation> {
|
||||
}
|
||||
return orderBy.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une cotisation mensuelle existe déjà pour ce membre, cette organisation,
|
||||
* cette année et ce mois. Utilisé pour éviter les doublons lors de la génération automatique.
|
||||
*/
|
||||
public boolean existsByMembreOrganisationAnneeAndMois(UUID membreId, UUID organisationId, int annee, int mois) {
|
||||
Long count = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM Cotisation c "
|
||||
+ "WHERE c.membre.id = :membreId AND c.organisation.id = :orgId "
|
||||
+ "AND c.annee = :annee AND c.mois = :mois",
|
||||
Long.class)
|
||||
.setParameter("membreId", membreId)
|
||||
.setParameter("orgId", organisationId)
|
||||
.setParameter("annee", annee)
|
||||
.setParameter("mois", mois)
|
||||
.getSingleResult();
|
||||
return count != null && count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,16 @@ public class DocumentRepository implements PanacheRepositoryBase<Document, UUID>
|
||||
public List<Document> findAllActifs() {
|
||||
return find("actif = true ORDER BY dateCreation DESC").list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve les documents créés par un utilisateur (par email)
|
||||
*
|
||||
* @param email Email de l'utilisateur (creePar)
|
||||
* @return Liste des documents
|
||||
*/
|
||||
public List<Document> findByCreePar(String email) {
|
||||
return find("creePar = ?1 AND actif = true ORDER BY dateCreation DESC", email).list();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.ParametresCotisationOrganisation;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class ParametresCotisationOrganisationRepository
|
||||
implements PanacheRepositoryBase<ParametresCotisationOrganisation, UUID> {
|
||||
|
||||
public Optional<ParametresCotisationOrganisation> findByOrganisationId(UUID organisationId) {
|
||||
return find("organisation.id = ?1", organisationId).firstResultOptional();
|
||||
}
|
||||
|
||||
/** Retourne toutes les organisations ayant activé la génération automatique. */
|
||||
public List<ParametresCotisationOrganisation> findAvecGenerationAutomatiqueActivee() {
|
||||
return find("generationAutomatiqueActivee = true AND actif = true").list();
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class AnalyticsResource {
|
||||
/** Calcule une métrique analytics pour une période donnée */
|
||||
@GET
|
||||
@Path("/metriques/{typeMetrique}")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Calculer une métrique analytics",
|
||||
description = "Calcule une métrique spécifique pour une période et organisation données")
|
||||
@@ -88,7 +88,7 @@ public class AnalyticsResource {
|
||||
/** Calcule les tendances d'un KPI sur une période */
|
||||
@GET
|
||||
@Path("/tendances/{typeMetrique}")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Calculer la tendance d'un KPI",
|
||||
description = "Calcule l'évolution et les tendances d'un KPI sur une période donnée")
|
||||
@@ -127,7 +127,7 @@ public class AnalyticsResource {
|
||||
/** Obtient tous les KPI pour une organisation */
|
||||
@GET
|
||||
@Path("/kpis")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Obtenir tous les KPI",
|
||||
description = "Récupère tous les KPI calculés pour une organisation et période données")
|
||||
@@ -163,7 +163,7 @@ public class AnalyticsResource {
|
||||
/** Calcule le KPI de performance globale */
|
||||
@GET
|
||||
@Path("/performance-globale")
|
||||
@RolesAllowed({"ADMIN", "MANAGER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN"})
|
||||
@Operation(
|
||||
summary = "Calculer la performance globale",
|
||||
description = "Calcule le score de performance globale de l'organisation")
|
||||
@@ -208,7 +208,7 @@ public class AnalyticsResource {
|
||||
/** Obtient les évolutions des KPI par rapport à la période précédente */
|
||||
@GET
|
||||
@Path("/evolutions")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Obtenir les évolutions des KPI",
|
||||
description = "Récupère les évolutions des KPI par rapport à la période précédente")
|
||||
@@ -248,7 +248,7 @@ public class AnalyticsResource {
|
||||
/** Obtient les widgets du tableau de bord pour un utilisateur */
|
||||
@GET
|
||||
@Path("/dashboard/widgets")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Obtenir les widgets du tableau de bord",
|
||||
description = "Récupère tous les widgets configurés pour le tableau de bord de l'utilisateur")
|
||||
@@ -285,7 +285,7 @@ public class AnalyticsResource {
|
||||
/** Obtient les types de métriques disponibles */
|
||||
@GET
|
||||
@Path("/types-metriques")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Obtenir les types de métriques disponibles",
|
||||
description = "Récupère la liste de tous les types de métriques disponibles")
|
||||
@@ -300,7 +300,7 @@ public class AnalyticsResource {
|
||||
/** Obtient les périodes d'analyse disponibles */
|
||||
@GET
|
||||
@Path("/periodes-analyse")
|
||||
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Operation(
|
||||
summary = "Obtenir les périodes d'analyse disponibles",
|
||||
description = "Récupère la liste de toutes les périodes d'analyse disponibles")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.service.ApprovalService;
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest;
|
||||
import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest;
|
||||
import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse;
|
||||
@@ -53,7 +54,7 @@ public class ApprovalResource {
|
||||
|
||||
if (transactionId == null || transactionType == null || amount == null) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("transactionId, transactionType et amount sont requis"))
|
||||
.entity(ErrorResponse.of("transactionId, transactionType et amount sont requis"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -64,12 +65,12 @@ public class ApprovalResource {
|
||||
return Response.status(Response.Status.CREATED).entity(approval).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la création de la demande d'approbation", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ public class ApprovalResource {
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération des approbations en attente", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -106,12 +107,12 @@ public class ApprovalResource {
|
||||
return Response.ok(approval).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération de l'approbation", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -131,16 +132,16 @@ public class ApprovalResource {
|
||||
return Response.ok(approval).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de l'approbation de la transaction", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -160,16 +161,16 @@ public class ApprovalResource {
|
||||
return Response.ok(approval).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors du rejet de la transaction", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -197,12 +198,12 @@ public class ApprovalResource {
|
||||
return Response.ok(approvals).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération de l'historique", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -221,12 +222,11 @@ public class ApprovalResource {
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors du comptage des approbations", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// Classes internes pour les réponses
|
||||
record ErrorResponse(String message) {}
|
||||
// Classe interne pour le comptage
|
||||
record CountResponse(long count) {}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class BackupResource {
|
||||
* Lister toutes les sauvegardes
|
||||
*/
|
||||
@GET
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Lister toutes les sauvegardes disponibles")
|
||||
public List<BackupResponse> getAllBackups() {
|
||||
log.info("GET /api/backups");
|
||||
@@ -48,7 +48,7 @@ public class BackupResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer une sauvegarde par ID")
|
||||
public BackupResponse getBackupById(@PathParam("id") UUID id) {
|
||||
log.info("GET /api/backups/{}", id);
|
||||
@@ -98,7 +98,7 @@ public class BackupResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/config")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer la configuration des sauvegardes automatiques")
|
||||
public BackupConfigResponse getBackupConfig() {
|
||||
log.info("GET /api/backups/config");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.service.BudgetService;
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest;
|
||||
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
@@ -51,12 +52,12 @@ public class BudgetResource {
|
||||
return Response.ok(budgets).build();
|
||||
} catch (BadRequestException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération des budgets", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -74,12 +75,12 @@ public class BudgetResource {
|
||||
return Response.ok(budget).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération du budget", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -98,16 +99,16 @@ public class BudgetResource {
|
||||
.build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (BadRequestException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la création du budget", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -125,12 +126,12 @@ public class BudgetResource {
|
||||
return Response.ok(tracking).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération du suivi budgétaire", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -150,16 +151,16 @@ public class BudgetResource {
|
||||
return Response.ok(budget).build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (BadRequestException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la mise à jour du budget", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -177,16 +178,15 @@ public class BudgetResource {
|
||||
return Response.noContent().build();
|
||||
} catch (NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la suppression du budget", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.of(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// Classe interne pour les réponses d'erreur
|
||||
record ErrorResponse(String message) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.comptabilite.request.*;
|
||||
import dev.lions.unionflow.server.api.dto.comptabilite.response.*;
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.service.ComptabiliteService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -52,12 +53,12 @@ public class ComptabiliteResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création du compte comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création du compte comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création du compte comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -76,12 +77,12 @@ public class ComptabiliteResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Compte comptable non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Compte comptable non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche du compte comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche du compte comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -100,7 +101,7 @@ public class ComptabiliteResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des comptes comptables");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des comptes comptables: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des comptes comptables: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -124,12 +125,12 @@ public class ComptabiliteResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création du journal comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création du journal comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création du journal comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -148,12 +149,12 @@ public class ComptabiliteResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Journal comptable non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Journal comptable non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche du journal comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche du journal comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche du journal comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -172,7 +173,7 @@ public class ComptabiliteResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des journaux comptables");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des journaux comptables: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des journaux comptables: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -196,12 +197,12 @@ public class ComptabiliteResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création de l'écriture comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création de l'écriture comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création de l'écriture comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -220,12 +221,12 @@ public class ComptabiliteResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Écriture comptable non trouvée"))
|
||||
.entity(ErrorResponse.ofError("Écriture comptable non trouvée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche de l'écriture comptable");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche de l'écriture comptable: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche de l'écriture comptable: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -245,7 +246,7 @@ public class ComptabiliteResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des écritures");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des écritures: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -265,17 +266,10 @@ public class ComptabiliteResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des écritures");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des écritures: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Classe interne pour les réponses d'erreur */
|
||||
public static class ErrorResponse {
|
||||
public String error;
|
||||
|
||||
public ErrorResponse(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -247,4 +247,55 @@ public class CompteAdherentResource {
|
||||
membreKeycloakSyncService.changerMotDePassePremierLogin(membreOpt.get().getId(), nouveauMotDePasse);
|
||||
return Response.ok(Map.of("message", "Mot de passe mis à jour avec succès.")).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint mobile : changement de mot de passe depuis l'app Flutter.
|
||||
* Bypass lions-user-manager — appel direct à l'API Admin Keycloak.
|
||||
*
|
||||
* <p>Body attendu : {@code { "userId": "...", "oldPassword": "...", "newPassword": "..." }}
|
||||
*/
|
||||
@POST
|
||||
@Path("/auth/change-password")
|
||||
@Authenticated
|
||||
@Operation(
|
||||
summary = "Changer le mot de passe (mobile)",
|
||||
description = "Endpoint dédié à l'application mobile. Bypass lions-user-manager via API Admin Keycloak directe."
|
||||
)
|
||||
public Response changerMotDePasseMobile(Map<String, String> body) {
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
String newPassword = body == null ? null : body.get("newPassword");
|
||||
if (newPassword == null || newPassword.isBlank()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "Le champ 'newPassword' est requis."))
|
||||
.build();
|
||||
}
|
||||
if (newPassword.length() < 8) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "Le mot de passe doit contenir au moins 8 caractères."))
|
||||
.build();
|
||||
}
|
||||
|
||||
Optional<Membre> membreOpt = membreRepository.findByEmail(email.trim())
|
||||
.or(() -> membreRepository.findByEmail(email.trim().toLowerCase()));
|
||||
|
||||
if (membreOpt.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Aucun membre trouvé pour ce compte."))
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
membreKeycloakSyncService.changerMotDePasseDirectKeycloak(membreOpt.get().getId(), newPassword);
|
||||
return Response.ok(Map.of("message", "Mot de passe mis à jour avec succès.")).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur changement mot de passe pour %s", email);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors du changement de mot de passe: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,19 +60,19 @@ public class CotisationResource {
|
||||
try {
|
||||
log.info("GET /api/cotisations/public - page: {}, size: {}", page, size);
|
||||
|
||||
List<CotisationSummaryResponse> cotisations = cotisationService.getAllCotisations(page, size);
|
||||
List<CotisationResponse> cotisations = cotisationService.getAllCotisations(page, size);
|
||||
|
||||
List<Map<String, Object>> content = cotisations.stream()
|
||||
.map(c -> {
|
||||
Map<String, Object> map = new java.util.HashMap<>();
|
||||
map.put("id", c.id().toString());
|
||||
map.put("reference", c.numeroReference());
|
||||
map.put("nomMembre", c.nomMembre());
|
||||
map.put("montantDu", c.montantDu());
|
||||
map.put("montantPaye", c.montantPaye());
|
||||
map.put("statut", c.statut());
|
||||
map.put("statutLibelle", c.statutLibelle());
|
||||
map.put("dateEcheance", c.dateEcheance().toString());
|
||||
map.put("id", c.getId() != null ? c.getId().toString() : null);
|
||||
map.put("reference", c.getNumeroReference());
|
||||
map.put("nomMembre", c.getNomMembre());
|
||||
map.put("montantDu", c.getMontantDu());
|
||||
map.put("montantPaye", c.getMontantPaye());
|
||||
map.put("statut", c.getStatut());
|
||||
map.put("statutLibelle", c.getStatutLibelle());
|
||||
map.put("dateEcheance", c.getDateEcheance() != null ? c.getDateEcheance().toString() : null);
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@@ -113,7 +113,7 @@ public class CotisationResource {
|
||||
|
||||
try {
|
||||
log.info("GET /api/cotisations - page: {}, size: {}", page, size);
|
||||
List<CotisationSummaryResponse> cotisations = cotisationService.getAllCotisations(page, size);
|
||||
List<CotisationResponse> cotisations = cotisationService.getAllCotisations(page, size);
|
||||
return Response.ok(cotisations).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lister cotisations", e);
|
||||
@@ -233,7 +233,7 @@ public class CotisationResource {
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("20") int size) {
|
||||
try {
|
||||
List<CotisationSummaryResponse> results = cotisationService.getCotisationsByMembre(membreId, page, size);
|
||||
List<CotisationResponse> results = cotisationService.getCotisationsByMembre(membreId, page, size);
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
|
||||
@@ -251,7 +251,7 @@ public class CotisationResource {
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("20") int size) {
|
||||
try {
|
||||
List<CotisationSummaryResponse> results = cotisationService.getCotisationsByStatut(statut, page, size);
|
||||
List<CotisationResponse> results = cotisationService.getCotisationsByStatut(statut, page, size);
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
|
||||
@@ -268,7 +268,7 @@ public class CotisationResource {
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("20") int size) {
|
||||
try {
|
||||
List<CotisationSummaryResponse> results = cotisationService.getCotisationsEnRetard(page, size);
|
||||
List<CotisationResponse> results = cotisationService.getCotisationsEnRetard(page, size);
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
|
||||
@@ -290,7 +290,7 @@ public class CotisationResource {
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("20") int size) {
|
||||
try {
|
||||
List<CotisationSummaryResponse> results = cotisationService.rechercherCotisations(
|
||||
List<CotisationResponse> results = cotisationService.rechercherCotisations(
|
||||
membreId, statut, typeCotisation, annee, mois, page, size);
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
@@ -399,7 +399,7 @@ public class CotisationResource {
|
||||
@QueryParam("size") @DefaultValue("50") int size) {
|
||||
try {
|
||||
log.info("GET /api/cotisations/mes-cotisations");
|
||||
List<CotisationSummaryResponse> results = cotisationService.getMesCotisations(page, size);
|
||||
List<CotisationResponse> results = cotisationService.getMesCotisations(page, size);
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur récupération mes cotisations", e);
|
||||
@@ -423,7 +423,7 @@ public class CotisationResource {
|
||||
public Response getMesCotisationsEnAttente() {
|
||||
try {
|
||||
log.info("GET /api/cotisations/mes-cotisations/en-attente");
|
||||
List<CotisationSummaryResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
return Response.ok(results).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur récupération mes cotisations en attente", e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.api.dto.document.request.CreateDocumentRequest;
|
||||
import dev.lions.unionflow.server.api.dto.document.response.DocumentResponse;
|
||||
import dev.lions.unionflow.server.api.dto.document.request.CreatePieceJointeRequest;
|
||||
@@ -62,7 +63,7 @@ public class DocumentResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création du document");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création du document: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création du document: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +88,7 @@ public class DocumentResource {
|
||||
try {
|
||||
if (file == null || file.fileName() == null) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Aucun fichier fourni"))
|
||||
.entity(ErrorResponse.ofError("Aucun fichier fourni"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -135,12 +136,31 @@ public class DocumentResource {
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warnf("Validation échouée pour upload: %s", e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'upload du fichier");
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse("Erreur lors de l'upload: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de l'upload: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les documents de l'utilisateur connecté
|
||||
*
|
||||
* @return Liste des documents du membre connecté
|
||||
*/
|
||||
@GET
|
||||
@Path("/mes-documents")
|
||||
public Response listerMesDocuments() {
|
||||
try {
|
||||
List<DocumentResponse> result = documentService.listerMesDocuments();
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des documents du membre connecté");
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la récupération des documents: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -159,12 +179,12 @@ public class DocumentResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Document non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Document non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche du document");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche du document: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche du document: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -184,14 +204,12 @@ public class DocumentResource {
|
||||
return Response.ok().build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Document non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Document non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'enregistrement du téléchargement");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(
|
||||
new ErrorResponse(
|
||||
"Erreur lors de l'enregistrement du téléchargement: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de l'enregistrement du téléchargement: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -211,12 +229,12 @@ public class DocumentResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création de la pièce jointe");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création de la pièce jointe: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création de la pièce jointe: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -236,17 +254,10 @@ public class DocumentResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des pièces jointes");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des pièces jointes: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des pièces jointes: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Classe interne pour les réponses d'erreur */
|
||||
public static class ErrorResponse {
|
||||
public String error;
|
||||
|
||||
public ErrorResponse(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import dev.lions.unionflow.server.entity.InscriptionEvenement;
|
||||
import dev.lions.unionflow.server.service.EvenementService;
|
||||
import io.quarkus.panache.common.Page;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import io.quarkus.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -53,6 +54,7 @@ public class EvenementResource {
|
||||
/** Endpoint de test public pour vérifier la connectivité */
|
||||
@GET
|
||||
@Path("/test")
|
||||
@PermitAll
|
||||
@Operation(summary = "Test de connectivité", description = "Endpoint public pour tester la connectivité")
|
||||
@APIResponse(responseCode = "200", description = "Test réussi")
|
||||
public Response testConnectivity() {
|
||||
@@ -69,6 +71,7 @@ public class EvenementResource {
|
||||
/** Endpoint de debug pour vérifier le chargement des données */
|
||||
@GET
|
||||
@Path("/count")
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN" })
|
||||
@Operation(summary = "Compter les événements", description = "Compte le nombre d'événements dans la base")
|
||||
@APIResponse(responseCode = "200", description = "Nombre d'événements")
|
||||
public Response countEvenements() {
|
||||
@@ -194,6 +197,7 @@ public class EvenementResource {
|
||||
/** Liste les événements publics */
|
||||
@GET
|
||||
@Path("/publics")
|
||||
@PermitAll
|
||||
@Operation(summary = "Événements publics")
|
||||
public Response evenementsPublics(
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@@ -379,6 +383,7 @@ public class EvenementResource {
|
||||
/** Liste des feedbacks d'un événement */
|
||||
@GET
|
||||
@Path("/{id}/feedbacks")
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
|
||||
@Operation(summary = "Liste des feedbacks de l'événement")
|
||||
@APIResponse(responseCode = "200", description = "Liste des feedbacks")
|
||||
public Response getFeedbacks(@PathParam("id") UUID evenementId) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import io.quarkus.security.PermitAll;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -15,6 +16,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
@Path("/api/status")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApplicationScoped
|
||||
@PermitAll
|
||||
@Tag(name = "Status", description = "API de statut du serveur")
|
||||
public class HealthResource {
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public class LogsMonitoringResource {
|
||||
*/
|
||||
@POST
|
||||
@Path("/logs/search")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Rechercher dans les logs système", description = "Recherche avec filtres (niveau, source, texte, dates)")
|
||||
public List<SystemLogResponse> searchLogs(@Valid LogSearchRequest request) {
|
||||
log.info("POST /api/logs/search - level={}, source={}", request.getLevel(), request.getSource());
|
||||
@@ -90,7 +90,7 @@ public class LogsMonitoringResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/monitoring/metrics")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR", "HR_MANAGER", "ACTIVE_MEMBER"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer les métriques système en temps réel")
|
||||
public SystemMetricsResponse getSystemMetrics() {
|
||||
log.debug("GET /api/monitoring/metrics");
|
||||
@@ -102,7 +102,7 @@ public class LogsMonitoringResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/alerts")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer toutes les alertes actives")
|
||||
public List<SystemAlertResponse> getActiveAlerts() {
|
||||
log.info("GET /api/alerts");
|
||||
@@ -114,7 +114,7 @@ public class LogsMonitoringResource {
|
||||
*/
|
||||
@POST
|
||||
@Path("/alerts/{id}/acknowledge")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Acquitter une alerte")
|
||||
public Response acknowledgeAlert(@PathParam("id") UUID id) {
|
||||
log.info("POST /api/alerts/{}/acknowledge", id);
|
||||
@@ -127,7 +127,7 @@ public class LogsMonitoringResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/alerts/config")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer la configuration des alertes système")
|
||||
public AlertConfigResponse getAlertConfig() {
|
||||
log.info("GET /api/alerts/config");
|
||||
|
||||
@@ -421,6 +421,55 @@ public class MembreResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les membres actifs (statut compte = ACTIF).
|
||||
* Utilisé notamment pour la création de campagnes de cotisations.
|
||||
*/
|
||||
@GET
|
||||
@Path("/actifs")
|
||||
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
|
||||
@Operation(summary = "Membres actifs", description = "Liste tous les membres dont le compte est actif")
|
||||
@APIResponse(responseCode = "200", description = "Liste des membres actifs")
|
||||
public Response getMembresActifs() {
|
||||
try {
|
||||
LOG.info("GET /api/membres/actifs");
|
||||
List<Membre> membres = membreService.listerMembresActifs();
|
||||
List<MembreResponse> membresDTO = membreService.convertToResponseList(membres);
|
||||
return Response.ok(membresDTO).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur récupération membres actifs");
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", e.getMessage())).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les membres d'une organisation spécifique (statut ACTIF dans l'organisation).
|
||||
* Utilisé pour la création de campagnes ciblées.
|
||||
*/
|
||||
@GET
|
||||
@Path("/organisation/{organisationId}")
|
||||
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
|
||||
@Operation(summary = "Membres d'une organisation", description = "Liste les membres actifs d'une organisation")
|
||||
@APIResponse(responseCode = "200", description = "Liste des membres")
|
||||
public Response getMembresParOrganisation(
|
||||
@Parameter(description = "UUID de l'organisation") @PathParam("organisationId") UUID organisationId) {
|
||||
try {
|
||||
LOG.infof("GET /api/membres/organisation/%s", organisationId);
|
||||
List<dev.lions.unionflow.server.entity.MembreOrganisation> liens =
|
||||
membreOrgRepository.findMembresActifsParOrganisation(organisationId);
|
||||
List<MembreResponse> membresDTO = liens.stream()
|
||||
.filter(mo -> mo.getMembre() != null)
|
||||
.map(mo -> membreService.convertToResponse(mo.getMembre()))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
return Response.ok(membresDTO).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur récupération membres organisation %s", organisationId);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", e.getMessage())).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/recherche")
|
||||
@Operation(summary = "Rechercher des membres par nom ou prénom")
|
||||
@@ -1248,6 +1297,23 @@ public class MembreResource {
|
||||
return Response.ok(Map.of("statut", updated.getStatutMembre())).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un membre par son numéro de membre (ex: MBR-0001).
|
||||
* Utilisé notamment pour la recherche de parrain lors de l'inscription.
|
||||
*/
|
||||
@GET
|
||||
@Path("/numero/{numeroMembre}")
|
||||
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "MEMBRE", "USER" })
|
||||
@Operation(summary = "Trouver un membre par son numéro")
|
||||
@APIResponse(responseCode = "200", description = "Membre trouvé")
|
||||
@APIResponse(responseCode = "404", description = "Membre non trouvé")
|
||||
public Response obtenirParNumero(@PathParam("numeroMembre") String numeroMembre) {
|
||||
LOG.infof("GET /api/membres/numero/%s", numeroMembre);
|
||||
Membre membre = membreService.trouverParNumeroMembre(numeroMembre)
|
||||
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec le numéro: " + numeroMembre));
|
||||
return Response.ok(membreService.convertToResponse(membre)).build();
|
||||
}
|
||||
|
||||
/** Résout l'UUID de l'admin connecté depuis le JWT subject. */
|
||||
private UUID resolveCurrentAdminId() {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.notification.request.CreateTemplateNotificationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.notification.response.NotificationResponse;
|
||||
@@ -61,7 +62,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur liste notifications membre connecté");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur liste notifications non lues");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -107,12 +108,12 @@ public class NotificationResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création du template");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création du template: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création du template: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -136,7 +137,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création de la notification");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création de la notification: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création de la notification: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -156,12 +157,12 @@ public class NotificationResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Notification non trouvée"))
|
||||
.entity(ErrorResponse.ofError("Notification non trouvée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors du marquage de la notification");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors du marquage de la notification: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors du marquage de la notification: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -180,12 +181,12 @@ public class NotificationResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Notification non trouvée"))
|
||||
.entity(ErrorResponse.ofError("Notification non trouvée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche de la notification");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche de la notification: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche de la notification: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -205,7 +206,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des notifications");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des notifications: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -225,9 +226,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des notifications non lues");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(
|
||||
new ErrorResponse(
|
||||
"Erreur lors de la liste des notifications non lues: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications non lues: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -246,9 +245,7 @@ public class NotificationResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des notifications en attente");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(
|
||||
new ErrorResponse(
|
||||
"Erreur lors de la liste des notifications en attente: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications en attente: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -269,27 +266,16 @@ public class NotificationResource {
|
||||
return Response.ok(Map.of("notificationsCreees", notificationsCreees)).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'envoi des notifications groupées");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(
|
||||
new ErrorResponse(
|
||||
"Erreur lors de l'envoi des notifications groupées: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de l'envoi des notifications groupées: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Classe interne pour les réponses d'erreur */
|
||||
public static class ErrorResponse {
|
||||
public String error;
|
||||
|
||||
public ErrorResponse(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Classe interne pour les requêtes de notifications groupées (WOU/DRY) */
|
||||
public static class NotificationGroupeeRequest {
|
||||
public List<UUID> membreIds;
|
||||
|
||||
@@ -170,6 +170,24 @@ public class PaiementResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling du statut d'une IntentionPaiement Wave.
|
||||
* Si Wave a confirmé le paiement, réconcilie automatiquement la cotisation (PAYEE) et retourne COMPLETEE.
|
||||
* Le client web appelle cet endpoint toutes les 3 secondes pendant l'affichage du QR code.
|
||||
*
|
||||
* @param intentionId UUID de l'intention (clientReference retourné par initier-paiement-en-ligne)
|
||||
* @return Statut courant + waveLaunchUrl (pour re-générer le QR si besoin) + message
|
||||
*/
|
||||
@GET
|
||||
@Path("/statut-intention/{intentionId}")
|
||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
||||
public Response getStatutIntention(@PathParam("intentionId") java.util.UUID intentionId) {
|
||||
LOG.infof("GET /api/paiements/statut-intention/%s", intentionId);
|
||||
dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse result =
|
||||
paiementService.verifierStatutIntention(intentionId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Déclare un paiement manuel (espèces, virement, chèque).
|
||||
* Le paiement est créé avec le statut EN_ATTENTE_VALIDATION.
|
||||
|
||||
@@ -4,6 +4,7 @@ import dev.lions.unionflow.server.api.dto.solidarite.request.CreatePropositionAi
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.request.UpdatePropositionAideRequest;
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.response.PropositionAideResponse;
|
||||
import dev.lions.unionflow.server.service.PropositionAideService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
@@ -21,6 +22,7 @@ import java.util.List;
|
||||
@Path("/api/propositions-aide")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
|
||||
@Tag(name = "Propositions d'aide", description = "Gestion des propositions d'aide solidarité")
|
||||
public class PropositionAideResource {
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package dev.lions.unionflow.server.resource;
|
||||
import dev.lions.unionflow.server.api.dto.role.response.RoleResponse;
|
||||
import dev.lions.unionflow.server.entity.Role;
|
||||
import dev.lions.unionflow.server.service.RoleService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -19,6 +20,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Path("/api/roles")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
|
||||
@Tag(name = "Rôles", description = "Gestion des rôles et permissions")
|
||||
public class RoleResource {
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
@@ -158,6 +159,36 @@ public class SouscriptionResource {
|
||||
|
||||
// ── SuperAdmin ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liste toutes les souscriptions (SuperAdmin dashboard), avec filtre optionnel par org.
|
||||
*/
|
||||
@GET
|
||||
@Path("/admin/toutes")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
public Response getSouscriptionsToutes(
|
||||
@QueryParam("organisationId") UUID organisationId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("1000") int size) {
|
||||
LOG.debug("GET /api/souscriptions/admin/toutes");
|
||||
List<SouscriptionStatutResponse> liste = souscriptionService.listerToutes(organisationId, page, size);
|
||||
return Response.ok(liste).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la souscription active d'une organisation (SuperAdmin).
|
||||
*/
|
||||
@GET
|
||||
@Path("/admin/organisation/{organisationId}/active")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
public Response getActiveParOrganisation(@PathParam("organisationId") UUID organisationId) {
|
||||
LOG.debugf("GET /api/souscriptions/admin/organisation/%s/active", organisationId);
|
||||
SouscriptionStatutResponse resp = souscriptionService.obtenirActiveParOrganisation(organisationId);
|
||||
if (resp == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
return Response.ok(resp).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les souscriptions en attente de validation SuperAdmin (statut PAIEMENT_CONFIRME).
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ public class SystemResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/config")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer la configuration système", description = "Retourne la configuration système complète")
|
||||
public SystemConfigResponse getSystemConfig() {
|
||||
log.info("GET /api/system/config");
|
||||
@@ -62,7 +62,7 @@ public class SystemResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/cache/stats")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Récupérer les statistiques du cache système")
|
||||
public CacheStatsResponse getCacheStats() {
|
||||
log.info("GET /api/system/cache/stats");
|
||||
@@ -111,7 +111,7 @@ public class SystemResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/metrics")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(
|
||||
summary = "Récupérer les métriques système en temps réel",
|
||||
description = "Retourne toutes les métriques système (CPU, RAM, disque, utilisateurs actifs, etc.)"
|
||||
|
||||
@@ -4,12 +4,13 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
|
||||
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.IntentionPaiement;
|
||||
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
|
||||
import dev.lions.unionflow.server.service.PaiementService;
|
||||
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -21,7 +22,6 @@ import org.jboss.logging.Logger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -50,18 +50,65 @@ public class WaveRedirectResource {
|
||||
@Inject
|
||||
TransactionEpargneService transactionEpargneService;
|
||||
|
||||
@Inject
|
||||
PaiementService paiementService;
|
||||
|
||||
@GET
|
||||
@Path("/success")
|
||||
@Transactional
|
||||
public Response success(@QueryParam("ref") String ref) {
|
||||
LOG.infof("Wave redirect success, ref=%s", ref);
|
||||
if (mockEnabled && ref != null && !ref.isBlank()) {
|
||||
applyMockCompletion(ref);
|
||||
LOG.infof("Wave redirect success (mobile), ref=%s", ref);
|
||||
if (ref != null && !ref.isBlank()) {
|
||||
applyCompletion(ref);
|
||||
}
|
||||
String location = buildDeepLink("success", ref);
|
||||
return Response.seeOther(URI.create(location)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint de redirection Wave pour le flux web QR code.
|
||||
* Appelé par Wave sur le téléphone du membre après paiement confirmé.
|
||||
* Marque la cotisation PAYEE et affiche une page HTML de confirmation.
|
||||
*/
|
||||
@GET
|
||||
@Path("/web-success")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
@Transactional
|
||||
public Response webSuccess(@QueryParam("ref") String ref) {
|
||||
LOG.infof("Wave redirect web-success, ref=%s", ref);
|
||||
if (ref != null && !ref.isBlank()) {
|
||||
applyCompletion(ref);
|
||||
}
|
||||
String html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Paiement confirmé</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;display:flex;flex-direction:column;align-items:center;
|
||||
justify-content:center;min-height:100vh;margin:0;background:#f0fdf4;}
|
||||
.card{background:#fff;border-radius:16px;padding:2rem 2.5rem;text-align:center;
|
||||
box-shadow:0 4px 24px #0001;max-width:360px;}
|
||||
.icon{font-size:3.5rem;margin-bottom:1rem;}
|
||||
h2{color:#16a34a;margin:.5rem 0;}
|
||||
p{color:#555;margin:.5rem 0;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">✅</div>
|
||||
<h2>Paiement confirmé !</h2>
|
||||
<p>Votre cotisation a été enregistrée avec succès.</p>
|
||||
<p style="font-size:.85rem;color:#888;margin-top:1rem;">
|
||||
Vous pouvez fermer cette page et revenir sur UnionFlow.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
return Response.ok(html).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/error")
|
||||
public Response error(@QueryParam("ref") String ref) {
|
||||
@@ -85,40 +132,31 @@ public class WaveRedirectResource {
|
||||
if (ref == null || ref.isBlank()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("ref requis").build();
|
||||
}
|
||||
applyMockCompletion(ref);
|
||||
applyCompletion(ref);
|
||||
return Response.seeOther(URI.create(buildDeepLink("success", ref))).build();
|
||||
}
|
||||
|
||||
/** En mode mock : marque l'intention COMPLETEE et les cotisations liées PAYEE (simulation Wave). */
|
||||
private void applyMockCompletion(String ref) {
|
||||
/**
|
||||
* Marque l'intention comme complétée et réconcilie les cotisations/dépôts liés.
|
||||
* Délègue au PaiementService pour les cotisations ; gère les dépôts épargne localement.
|
||||
*/
|
||||
private void applyCompletion(String ref) {
|
||||
try {
|
||||
UUID intentionId = UUID.fromString(ref.trim());
|
||||
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
|
||||
if (intention == null) {
|
||||
LOG.warnf("Intention non trouvée pour mock: %s", ref);
|
||||
LOG.warnf("Intention non trouvée: %s", ref);
|
||||
return;
|
||||
}
|
||||
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
|
||||
intention.setDateCompletion(LocalDateTime.now());
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
// Gérer les dépôts épargne (non couverts par PaiementService)
|
||||
String objetsCibles = intention.getObjetsCibles();
|
||||
if (objetsCibles != null && !objetsCibles.isBlank()) {
|
||||
JsonNode arr = OBJECT_MAPPER.readTree(objetsCibles);
|
||||
if (arr.isArray()) {
|
||||
for (JsonNode node : arr) {
|
||||
if (node.has("type") && "COTISATION".equals(node.get("type").asText()) && node.has("id")) {
|
||||
UUID cotisationId = UUID.fromString(node.get("id").asText());
|
||||
Cotisation cotisation = intentionPaiementRepository.getEntityManager().find(Cotisation.class, cotisationId);
|
||||
if (cotisation != null) {
|
||||
BigDecimal montant = node.has("montant") ? new BigDecimal(node.get("montant").asText()) : cotisation.getMontantDu();
|
||||
cotisation.setMontantPaye(montant);
|
||||
cotisation.setStatut("PAYEE");
|
||||
cotisation.setDatePaiement(LocalDateTime.now());
|
||||
intentionPaiementRepository.getEntityManager().merge(cotisation);
|
||||
LOG.infof("Mock Wave: cotisation %s marquée PAYEE", cotisationId);
|
||||
}
|
||||
} else if (node.has("type") && "DEPOT_EPARGNE".equals(node.get("type").asText()) && node.has("compteId") && node.has("montant")) {
|
||||
if ("DEPOT_EPARGNE".equals(node.path("type").asText())
|
||||
&& node.has("compteId") && node.has("montant")) {
|
||||
String compteId = node.get("compteId").asText();
|
||||
BigDecimal montant = new BigDecimal(node.get("montant").asText());
|
||||
TransactionEpargneRequest req = TransactionEpargneRequest.builder()
|
||||
@@ -128,14 +166,17 @@ public class WaveRedirectResource {
|
||||
.motif("Dépôt via Wave (mobile money)")
|
||||
.build();
|
||||
transactionEpargneService.executerTransaction(req);
|
||||
LOG.infof("Mock Wave: dépôt épargne %s XOF sur compte %s", montant, compteId);
|
||||
LOG.infof("Wave: dépôt épargne %s XOF sur compte %s", montant, compteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.infof("Mock Wave: intention %s complétée (validation simulée)", ref);
|
||||
|
||||
// Déléguer la complétion cotisations au service
|
||||
paiementService.completerIntention(intention, null);
|
||||
LOG.infof("Wave: intention %s complétée", ref);
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Mock Wave: erreur applyMockCompletion ref=%s", ref);
|
||||
LOG.errorf(e, "Wave: erreur applyCompletion ref=%s", ref);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
|
||||
import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO;
|
||||
import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO;
|
||||
import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
|
||||
@@ -56,12 +57,12 @@ public class WaveResource {
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.entity(ErrorResponse.ofError(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création du compte Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création du compte Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création du compte Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -83,12 +84,12 @@ public class WaveResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Compte Wave non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la mise à jour du compte Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la mise à jour du compte Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la mise à jour du compte Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -109,12 +110,12 @@ public class WaveResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Compte Wave non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la vérification du compte Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la vérification du compte Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la vérification du compte Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -133,12 +134,12 @@ public class WaveResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Compte Wave non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche du compte Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -156,14 +157,14 @@ public class WaveResource {
|
||||
CompteWaveDTO result = waveService.trouverCompteWaveParTelephone(numeroTelephone);
|
||||
if (result == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Compte Wave non trouvé"))
|
||||
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
|
||||
.build();
|
||||
}
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche du compte Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -183,7 +184,7 @@ public class WaveResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la liste des comptes Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la liste des comptes Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la liste des comptes Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -207,7 +208,7 @@ public class WaveResource {
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la création de la transaction Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la création de la transaction Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la création de la transaction Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -229,14 +230,12 @@ public class WaveResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Transaction Wave non trouvée"))
|
||||
.entity(ErrorResponse.ofError("Transaction Wave non trouvée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la mise à jour du statut de la transaction Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(
|
||||
new ErrorResponse(
|
||||
"Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -255,22 +254,15 @@ public class WaveResource {
|
||||
return Response.ok(result).build();
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Transaction Wave non trouvée"))
|
||||
.entity(ErrorResponse.ofError("Transaction Wave non trouvée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche de la transaction Wave");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse("Erreur lors de la recherche de la transaction Wave: " + e.getMessage()))
|
||||
.entity(ErrorResponse.ofError("Erreur lors de la recherche de la transaction Wave: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Classe interne pour les réponses d'erreur */
|
||||
public static class ErrorResponse {
|
||||
public String error;
|
||||
|
||||
public ErrorResponse(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package dev.lions.unionflow.server.scheduler;
|
||||
|
||||
import dev.lions.unionflow.server.service.MemberLifecycleService;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Planificateur automatique pour le cycle de vie des membres.
|
||||
*
|
||||
* <p>Tâches exécutées périodiquement :
|
||||
* <ul>
|
||||
* <li>Toutes les heures : rappels d'invitation expirant dans 24h</li>
|
||||
* <li>Tous les jours à 02:00 : expiration des invitations périmées</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Utilise le module {@code quarkus-scheduler} (déjà dans le pom.xml).
|
||||
* En production, les expressions cron peuvent être configurées via :
|
||||
* {@code unionflow.lifecycle.invitation.expiry.cron} dans application.properties.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MemberLifecycleScheduler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MemberLifecycleScheduler.class);
|
||||
|
||||
@Inject
|
||||
MemberLifecycleService lifecycleService;
|
||||
|
||||
/**
|
||||
* Envoie des rappels pour les invitations qui expirent dans les 24 prochaines heures.
|
||||
* Exécuté toutes les heures.
|
||||
*/
|
||||
@Scheduled(cron = "${unionflow.lifecycle.reminder.cron:0 0 * * * ?}",
|
||||
identity = "membre-invitation-reminder")
|
||||
void rappelerInvitationsExpirantBientot() {
|
||||
try {
|
||||
int count = lifecycleService.envoyerRappelsInvitation();
|
||||
if (count > 0) {
|
||||
LOG.infof("[Scheduler] Rappels d'invitation envoyés : %d", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "[Scheduler] Erreur lors de l'envoi des rappels d'invitation");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire les invitations dont la date limite est dépassée.
|
||||
* Exécuté chaque nuit à 02:00.
|
||||
*/
|
||||
@Scheduled(cron = "${unionflow.lifecycle.expiry.cron:0 0 2 * * ?}",
|
||||
identity = "membre-invitation-expiry")
|
||||
void expirerInvitations() {
|
||||
try {
|
||||
int count = lifecycleService.expirerInvitations();
|
||||
if (count > 0) {
|
||||
LOG.infof("[Scheduler] Invitations expirées traitées : %d", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "[Scheduler] Erreur lors de l'expiration des invitations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.service.OrganisationModuleService;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.container.ResourceInfo;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Filtre JAX-RS qui applique le contrôle d'accès par module métier.
|
||||
*
|
||||
* <p>S'exécute après l'authentification (AUTHORIZATION + 10) pour laisser
|
||||
* Quarkus OIDC et {@code @RolesAllowed} s'exécuter en premier.
|
||||
*
|
||||
* <p>Le filtre :
|
||||
* <ol>
|
||||
* <li>Détecte l'annotation {@link RequiresModule} sur la méthode ou la classe.</li>
|
||||
* <li>Extrait l'organisation active depuis le header {@code X-Active-Organisation-Id}.</li>
|
||||
* <li>Vérifie via {@link OrganisationModuleService} que le module est activé.</li>
|
||||
* <li>Retourne HTTP 403 avec un message explicite si le module est absent.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Header attendu : {@code X-Active-Organisation-Id: <UUID>}
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHORIZATION + 10)
|
||||
public class ModuleAccessFilter implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ModuleAccessFilter.class);
|
||||
|
||||
/** Nom du header HTTP transportant l'UUID de l'organisation active. */
|
||||
public static final String HEADER_ACTIVE_ORG = "X-Active-Organisation-Id";
|
||||
|
||||
@Inject
|
||||
OrganisationModuleService organisationModuleService;
|
||||
|
||||
@Context
|
||||
ResourceInfo resourceInfo;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) throws IOException {
|
||||
// 1. Détecter @RequiresModule sur la méthode, puis sur la classe
|
||||
Method method = resourceInfo.getResourceMethod();
|
||||
RequiresModule annotation = method != null ? method.getAnnotation(RequiresModule.class) : null;
|
||||
if (annotation == null && resourceInfo.getResourceClass() != null) {
|
||||
annotation = resourceInfo.getResourceClass().getAnnotation(RequiresModule.class);
|
||||
}
|
||||
|
||||
if (annotation == null) {
|
||||
// Aucune annotation — laisser passer
|
||||
return;
|
||||
}
|
||||
|
||||
String moduleRequis = annotation.value().toUpperCase();
|
||||
|
||||
// 2. Extraire l'organisation active depuis le header
|
||||
String orgIdHeader = requestContext.getHeaderString(HEADER_ACTIVE_ORG);
|
||||
if (orgIdHeader == null || orgIdHeader.isBlank()) {
|
||||
LOG.warnf("@RequiresModule(%s) — header %s absent, accès refusé", moduleRequis, HEADER_ACTIVE_ORG);
|
||||
requestContext.abortWith(buildForbiddenResponse(
|
||||
"Le header " + HEADER_ACTIVE_ORG + " est requis pour accéder à ce module.",
|
||||
moduleRequis));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID organisationId;
|
||||
try {
|
||||
organisationId = UUID.fromString(orgIdHeader.trim());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warnf("@RequiresModule(%s) — header %s invalide : %s", moduleRequis, HEADER_ACTIVE_ORG, orgIdHeader);
|
||||
requestContext.abortWith(buildForbiddenResponse(
|
||||
"La valeur du header " + HEADER_ACTIVE_ORG + " n'est pas un UUID valide.",
|
||||
moduleRequis));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Vérifier l'activation du module
|
||||
if (!organisationModuleService.isModuleActif(organisationId, moduleRequis)) {
|
||||
String message = annotation.message().isBlank()
|
||||
? "Le module '" + moduleRequis + "' n'est pas activé pour cette organisation."
|
||||
: annotation.message();
|
||||
LOG.infof("@RequiresModule(%s) — module inactif pour l'organisation %s", moduleRequis, organisationId);
|
||||
requestContext.abortWith(buildForbiddenResponse(message, moduleRequis));
|
||||
}
|
||||
}
|
||||
|
||||
private Response buildForbiddenResponse(String message, String module) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of(
|
||||
"error", "MODULE_NOT_ACTIVE",
|
||||
"message", message,
|
||||
"module", module
|
||||
))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Filtre JAX-RS qui résout l'organisation active pour chaque requête.
|
||||
*
|
||||
* <p>Lit le header {@code X-Active-Organisation-Id}, valide que l'utilisateur
|
||||
* connecté est bien membre actif de cette organisation, puis peuple
|
||||
* {@link OrganisationContextHolder} pour les services aval.
|
||||
*
|
||||
* <p>Si le header est absent, le contexte reste non résolu (les endpoints
|
||||
* qui ne nécessitent pas de contexte org continuent de fonctionner normalement).
|
||||
* Si le header est présent mais invalide ou si l'utilisateur n'est pas membre,
|
||||
* la requête est rejetée avec HTTP 403.
|
||||
*
|
||||
* <p>Priorité : AUTHORIZATION + 5 (après auth, avant @RequiresModule à +10).
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHORIZATION + 5)
|
||||
public class OrganisationContextFilter implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(OrganisationContextFilter.class);
|
||||
public static final String HEADER_ACTIVE_ORG = ModuleAccessFilter.HEADER_ACTIVE_ORG;
|
||||
|
||||
@Inject
|
||||
OrganisationContextHolder contextHolder;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrganisationRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) throws IOException {
|
||||
String orgIdHeader = requestContext.getHeaderString(HEADER_ACTIVE_ORG);
|
||||
|
||||
if (orgIdHeader == null || orgIdHeader.isBlank()) {
|
||||
// Pas de contexte org — les endpoints sans contexte fonctionnent normalement
|
||||
return;
|
||||
}
|
||||
|
||||
// Valider le format UUID
|
||||
UUID organisationId;
|
||||
try {
|
||||
organisationId = UUID.fromString(orgIdHeader.trim());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warnf("Header %s invalide : %s", HEADER_ACTIVE_ORG, orgIdHeader);
|
||||
requestContext.abortWith(jakarta.ws.rs.core.Response
|
||||
.status(jakarta.ws.rs.core.Response.Status.BAD_REQUEST)
|
||||
.entity(java.util.Map.of("error", "INVALID_ORGANISATION_ID",
|
||||
"message", "Le header X-Active-Organisation-Id n'est pas un UUID valide."))
|
||||
.build());
|
||||
return;
|
||||
}
|
||||
|
||||
// Résoudre l'organisation
|
||||
Optional<Organisation> orgOpt = organisationRepository.findByIdOptional(organisationId);
|
||||
if (orgOpt.isEmpty()) {
|
||||
LOG.warnf("Organisation introuvable : %s", organisationId);
|
||||
requestContext.abortWith(jakarta.ws.rs.core.Response
|
||||
.status(jakarta.ws.rs.core.Response.Status.NOT_FOUND)
|
||||
.entity(java.util.Map.of("error", "ORGANISATION_NOT_FOUND",
|
||||
"message", "Organisation introuvable."))
|
||||
.build());
|
||||
return;
|
||||
}
|
||||
|
||||
Organisation organisation = orgOpt.get();
|
||||
|
||||
// Vérifier l'appartenance (skip pour SUPERADMIN qui a accès à tout)
|
||||
if (!securityIdentity.isAnonymous()) {
|
||||
boolean isSuperAdmin = securityIdentity.getRoles().contains("SUPERADMIN")
|
||||
|| securityIdentity.getRoles().contains("SUPER_ADMIN")
|
||||
|| securityIdentity.getRoles().contains("ADMIN");
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
String email = securityIdentity.getPrincipal().getName();
|
||||
Optional<Membre> membreOpt = membreRepository.findByEmail(email);
|
||||
|
||||
if (membreOpt.isPresent()) {
|
||||
Optional<MembreOrganisation> membreOrgOpt = membreOrganisationRepository
|
||||
.findByMembreIdAndOrganisationId(membreOpt.get().getId(), organisationId);
|
||||
|
||||
if (membreOrgOpt.isEmpty()) {
|
||||
LOG.warnf("Utilisateur %s n'est pas membre de l'organisation %s", email, organisationId);
|
||||
requestContext.abortWith(jakarta.ws.rs.core.Response
|
||||
.status(jakarta.ws.rs.core.Response.Status.FORBIDDEN)
|
||||
.entity(java.util.Map.of("error", "NOT_MEMBER_OF_ORGANISATION",
|
||||
"message", "Vous n'êtes pas membre de cette organisation."))
|
||||
.build());
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le membre est actif
|
||||
MembreOrganisation membreOrg = membreOrgOpt.get();
|
||||
String statut = membreOrg.getStatutMembre() != null
|
||||
? membreOrg.getStatutMembre().name() : "";
|
||||
if (!"ACTIF".equals(statut)) {
|
||||
LOG.warnf("Membre %s statut non-actif (%s) dans l'organisation %s", email, statut, organisationId);
|
||||
requestContext.abortWith(jakarta.ws.rs.core.Response
|
||||
.status(jakarta.ws.rs.core.Response.Status.FORBIDDEN)
|
||||
.entity(java.util.Map.of("error", "MEMBER_NOT_ACTIVE",
|
||||
"message", "Votre adhésion à cette organisation n'est pas active."))
|
||||
.build());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Peupler le contexte
|
||||
contextHolder.setOrganisationId(organisationId);
|
||||
contextHolder.setOrganisation(organisation);
|
||||
contextHolder.setResolved(true);
|
||||
LOG.debugf("Contexte org résolu : %s (%s)", organisation.getNom(), organisationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Holder request-scoped contenant l'organisation active résolue pour la requête courante.
|
||||
*
|
||||
* <p>Peuplé par {@link OrganisationContextFilter} à partir du header
|
||||
* {@code X-Active-Organisation-Id}. Utilisé par les services métier pour
|
||||
* scoper toutes les opérations à l'organisation active.
|
||||
*
|
||||
* <p>Exemple d'utilisation dans un service :
|
||||
* <pre>{@code
|
||||
* @Inject OrganisationContextHolder orgContext;
|
||||
*
|
||||
* public List<Tontine> listTontines() {
|
||||
* UUID orgId = orgContext.getOrganisationId();
|
||||
* return tontineRepository.findByOrganisationId(orgId);
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
@RequestScoped
|
||||
public class OrganisationContextHolder {
|
||||
|
||||
private UUID organisationId;
|
||||
private Organisation organisation;
|
||||
private boolean resolved = false;
|
||||
|
||||
public UUID getOrganisationId() {
|
||||
return organisationId;
|
||||
}
|
||||
|
||||
public void setOrganisationId(UUID organisationId) {
|
||||
this.organisationId = organisationId;
|
||||
}
|
||||
|
||||
public Organisation getOrganisation() {
|
||||
return organisation;
|
||||
}
|
||||
|
||||
public void setOrganisation(Organisation organisation) {
|
||||
this.organisation = organisation;
|
||||
}
|
||||
|
||||
public boolean isResolved() {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public void setResolved(boolean resolved) {
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne true si un contexte d'organisation est disponible.
|
||||
*/
|
||||
public boolean hasContext() {
|
||||
return resolved && organisationId != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation déclarative pour le contrôle d'accès par module métier.
|
||||
*
|
||||
* <p>Placée sur un endpoint JAX-RS, elle vérifie que l'organisation active
|
||||
* du contexte de la requête a bien le module requis activé.
|
||||
*
|
||||
* <p>Le module est déterminé par le type d'organisation (Option C) et non
|
||||
* par le plan tarifaire.
|
||||
*
|
||||
* <p>Exemple d'utilisation :
|
||||
* <pre>{@code
|
||||
* @GET
|
||||
* @Path("/cycles")
|
||||
* @RequiresModule("TONTINE")
|
||||
* @RolesAllowed({"ADMIN", "TONTINE_MANAGER"})
|
||||
* public Response getCycles() { ... }
|
||||
* }</pre>
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequiresModule {
|
||||
|
||||
/**
|
||||
* Nom du module requis (ex: "TONTINE", "CREDIT", "EPARGNE", "AGRICULTURE").
|
||||
* Doit correspondre à l'une des valeurs de {@link dev.lions.unionflow.server.service.OrganisationModuleService#MODULES_COMMUNS}
|
||||
* ou des modules métier définis par type d'organisation.
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* Message d'erreur personnalisé affiché si le module n'est pas actif.
|
||||
* Par défaut, un message générique est généré.
|
||||
*/
|
||||
String message() default "";
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
/**
|
||||
* Constantes centralisées pour les rôles UnionFlow.
|
||||
*
|
||||
* <p>Utiliser ces constantes dans {@code @RolesAllowed} pour éviter
|
||||
* les fautes de frappe et garantir la cohérence entre ressources.</p>
|
||||
*/
|
||||
public final class RoleConstant {
|
||||
|
||||
// ── Rôles système ─────────────────────────────────────────────────────────
|
||||
public static final String SUPER_ADMIN = "SUPER_ADMIN";
|
||||
public static final String ADMIN = "ADMIN";
|
||||
|
||||
// ── Rôles organisation ────────────────────────────────────────────────────
|
||||
public static final String ADMIN_ORGANISATION = "ADMIN_ORGANISATION";
|
||||
public static final String PRESIDENT = "PRESIDENT";
|
||||
public static final String VICE_PRESIDENT = "VICE_PRESIDENT";
|
||||
public static final String SECRETAIRE = "SECRETAIRE";
|
||||
public static final String TRESORIER = "TRESORIER";
|
||||
public static final String MODERATEUR = "MODERATEUR";
|
||||
|
||||
// ── Rôles membres ─────────────────────────────────────────────────────────
|
||||
public static final String MEMBRE = "MEMBRE";
|
||||
public static final String USER = "USER";
|
||||
|
||||
// ── Rôles fonctionnels (événements) ───────────────────────────────────────
|
||||
public static final String ORGANISATEUR_EVENEMENT = "ORGANISATEUR_EVENEMENT";
|
||||
|
||||
// ── Rôles modules spécialisés ─────────────────────────────────────────────
|
||||
public static final String TONTINE_RESP = "TONTINE_RESP";
|
||||
public static final String MUTUELLE_RESP = "MUTUELLE_RESP";
|
||||
public static final String VOTE_RESP = "VOTE_RESP";
|
||||
public static final String COOP_RESP = "COOP_RESP";
|
||||
public static final String ONG_RESP = "ONG_RESP";
|
||||
public static final String CULTE_RESP = "CULTE_RESP";
|
||||
public static final String REGISTRE_RESP = "REGISTRE_RESP";
|
||||
public static final String AGRI_RESP = "AGRI_RESP";
|
||||
public static final String COLLECTE_RESP = "COLLECTE_RESP";
|
||||
|
||||
private RoleConstant() {}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.entity.BaremeCotisationRole;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.entity.ParametresCotisationOrganisation;
|
||||
import dev.lions.unionflow.server.repository.BaremeCotisationRoleRepository;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.ParametresCotisationOrganisationRepository;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Génération automatique mensuelle des cotisations pour les organisations
|
||||
* ayant activé {@code generationAutomatiqueActivee} dans leurs paramètres.
|
||||
*
|
||||
* <p>Le job s'exécute le 1er de chaque mois à 1h00. Pour chaque membre actif,
|
||||
* il détermine le montant selon le barème du rôle fonctionnel de ce membre dans
|
||||
* l'organisation, ou utilise le montant par défaut si aucun barème n'est configuré.
|
||||
*
|
||||
* <p>Les cotisations déjà générées pour le même mois/année sont ignorées (idempotence).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class CotisationAutoGenerationService {
|
||||
|
||||
@Inject
|
||||
ParametresCotisationOrganisationRepository parametresRepository;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrganisationRepository;
|
||||
|
||||
@Inject
|
||||
BaremeCotisationRoleRepository baremeRepository;
|
||||
|
||||
@Inject
|
||||
CotisationRepository cotisationRepository;
|
||||
|
||||
/**
|
||||
* Point d'entrée du job planifié : 1er de chaque mois à 01:00.
|
||||
*/
|
||||
@Scheduled(cron = "0 0 1 1 * ?")
|
||||
@Transactional
|
||||
public void genererCotisationsMensuelles() {
|
||||
LocalDate aujourd_hui = LocalDate.now();
|
||||
int annee = aujourd_hui.getYear();
|
||||
int mois = aujourd_hui.getMonthValue();
|
||||
log.info("=== Génération automatique cotisations {}/{} ===", mois, annee);
|
||||
|
||||
List<ParametresCotisationOrganisation> paramsList =
|
||||
parametresRepository.findAvecGenerationAutomatiqueActivee();
|
||||
|
||||
if (paramsList.isEmpty()) {
|
||||
log.info("Aucune organisation avec génération automatique activée.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalCrees = 0;
|
||||
int totalIgnores = 0;
|
||||
|
||||
for (ParametresCotisationOrganisation params : paramsList) {
|
||||
Organisation org = params.getOrganisation();
|
||||
int[] result = genererPourOrganisation(org, params, annee, mois);
|
||||
totalCrees += result[0];
|
||||
totalIgnores += result[1];
|
||||
}
|
||||
|
||||
log.info("=== Génération terminée : {} créées, {} ignorées (déjà existantes) ===",
|
||||
totalCrees, totalIgnores);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les cotisations pour une organisation donnée.
|
||||
*
|
||||
* @return tableau [nbCreees, nbIgnorees]
|
||||
*/
|
||||
int[] genererPourOrganisation(Organisation org,
|
||||
ParametresCotisationOrganisation params,
|
||||
int annee, int mois) {
|
||||
List<MembreOrganisation> membresActifs =
|
||||
membreOrganisationRepository.findMembresActifsParOrganisation(org.getId());
|
||||
|
||||
int crees = 0;
|
||||
int ignores = 0;
|
||||
String devise = params.getDevise() != null ? params.getDevise() : "XOF";
|
||||
|
||||
for (MembreOrganisation mo : membresActifs) {
|
||||
if (mo.getMembre() == null) continue;
|
||||
|
||||
// Idempotence : ne pas recréer si déjà générée ce mois
|
||||
if (cotisationRepository.existsByMembreOrganisationAnneeAndMois(
|
||||
mo.getMembre().getId(), org.getId(), annee, mois)) {
|
||||
ignores++;
|
||||
continue;
|
||||
}
|
||||
|
||||
BigDecimal montant = resoudreMontantMensuel(org.getId(), mo.getRoleOrg(), params);
|
||||
if (montant == null || montant.compareTo(BigDecimal.ZERO) == 0) {
|
||||
log.debug("Org {} membre {} : montant 0, cotisation ignorée", org.getNom(), mo.getMembre().getId());
|
||||
ignores++;
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalDate echeance = LocalDate.of(annee, mois, 28);
|
||||
Cotisation cotisation = Cotisation.builder()
|
||||
.typeCotisation("MENSUELLE")
|
||||
.libelle("Cotisation mensuelle " + String.format("%02d/%d", mois, annee))
|
||||
.description("Génération automatique — " + org.getNom())
|
||||
.montantDu(montant)
|
||||
.montantPaye(BigDecimal.ZERO)
|
||||
.codeDevise(devise)
|
||||
.statut("EN_ATTENTE")
|
||||
.dateEcheance(echeance)
|
||||
.annee(annee)
|
||||
.mois(mois)
|
||||
.recurrente(true)
|
||||
.membre(mo.getMembre())
|
||||
.organisation(org)
|
||||
.build();
|
||||
cotisation.setNumeroReference(Cotisation.genererNumeroReference());
|
||||
cotisationRepository.persist(cotisation);
|
||||
crees++;
|
||||
}
|
||||
|
||||
log.info("Org '{}' [{}/{}] : {} cotisation(s) créée(s), {} ignorée(s)",
|
||||
org.getNom(), mois, annee, crees, ignores);
|
||||
return new int[]{crees, ignores};
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine le montant mensuel applicable à ce membre selon son rôle.
|
||||
* Priorité : barème du rôle → montant par défaut de l'organisation.
|
||||
*/
|
||||
private BigDecimal resoudreMontantMensuel(java.util.UUID orgId, String roleOrg,
|
||||
ParametresCotisationOrganisation params) {
|
||||
if (roleOrg != null && !roleOrg.isBlank()) {
|
||||
Optional<BaremeCotisationRole> bareme =
|
||||
baremeRepository.findByOrganisationIdAndRoleOrg(orgId, roleOrg);
|
||||
if (bareme.isPresent() && bareme.get().getMontantMensuel() != null) {
|
||||
return bareme.get().getMontantMensuel();
|
||||
}
|
||||
}
|
||||
return params.getMontantCotisationMensuelle();
|
||||
}
|
||||
}
|
||||
@@ -70,17 +70,17 @@ public class CotisationService {
|
||||
* @param size taille de la page
|
||||
* @return liste des cotisations converties en Summary Response
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getAllCotisations(int page, int size) {
|
||||
public List<CotisationResponse> getAllCotisations(int page, int size) {
|
||||
log.debug("Récupération des cotisations - page: {}, size: {}", page, size);
|
||||
|
||||
jakarta.persistence.TypedQuery<Cotisation> query = cotisationRepository.getEntityManager().createQuery(
|
||||
"SELECT c FROM Cotisation c ORDER BY c.dateEcheance DESC",
|
||||
"SELECT c FROM Cotisation c LEFT JOIN FETCH c.membre LEFT JOIN FETCH c.organisation ORDER BY c.dateEcheance DESC",
|
||||
Cotisation.class);
|
||||
query.setFirstResult(page * size);
|
||||
query.setMaxResults(size);
|
||||
List<Cotisation> cotisations = query.getResultList();
|
||||
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +283,7 @@ public class CotisationService {
|
||||
/**
|
||||
* Récupère les cotisations d'un membre.
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getCotisationsByMembre(@NotNull UUID membreId, int page, int size) {
|
||||
public List<CotisationResponse> getCotisationsByMembre(@NotNull UUID membreId, int page, int size) {
|
||||
log.debug("Récupération des cotisations du membre: {}", membreId);
|
||||
|
||||
if (!membreRepository.findByIdOptional(membreId).isPresent()) {
|
||||
@@ -293,35 +293,35 @@ public class CotisationService {
|
||||
List<Cotisation> cotisations = cotisationRepository.findByMembreId(
|
||||
membreId, Page.of(page, size), Sort.by("dateEcheance").descending());
|
||||
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les cotisations par statut.
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getCotisationsByStatut(@NotNull String statut, int page, int size) {
|
||||
public List<CotisationResponse> getCotisationsByStatut(@NotNull String statut, int page, int size) {
|
||||
log.debug("Récupération des cotisations avec statut: {}", statut);
|
||||
|
||||
List<Cotisation> cotisations = cotisationRepository.findByStatut(statut, Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les cotisations en retard.
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getCotisationsEnRetard(int page, int size) {
|
||||
public List<CotisationResponse> getCotisationsEnRetard(int page, int size) {
|
||||
log.debug("Récupération des cotisations en retard");
|
||||
|
||||
List<Cotisation> cotisations = cotisationRepository.findCotisationsEnRetard(LocalDate.now(), Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche avancée de cotisations.
|
||||
*/
|
||||
public List<CotisationSummaryResponse> rechercherCotisations(
|
||||
public List<CotisationResponse> rechercherCotisations(
|
||||
UUID membreId,
|
||||
String statut,
|
||||
String typeCotisation,
|
||||
@@ -334,7 +334,7 @@ public class CotisationService {
|
||||
List<Cotisation> cotisations = cotisationRepository.rechercheAvancee(
|
||||
membreId, statut, typeCotisation, annee, mois, Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -699,7 +699,7 @@ public class CotisationService {
|
||||
* Toutes les cotisations du membre connecté (tous statuts), ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
|
||||
* Utilisé pour les onglets Toutes / Payées / Dues / Retard.
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getMesCotisations(int page, int size) {
|
||||
public List<CotisationResponse> getMesCotisations(int page, int size) {
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
return Collections.emptyList();
|
||||
@@ -714,7 +714,7 @@ public class CotisationService {
|
||||
Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
List<Cotisation> cotisations = cotisationRepository.findByOrganisationIdIn(
|
||||
orgIds, Page.of(page, size), Sort.by("dateEcheance").descending());
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
|
||||
if (membreConnecte == null) {
|
||||
@@ -731,7 +731,7 @@ public class CotisationService {
|
||||
*
|
||||
* @return Liste des cotisations en attente
|
||||
*/
|
||||
public List<CotisationSummaryResponse> getMesCotisationsEnAttente() {
|
||||
public List<CotisationResponse> getMesCotisationsEnAttente() {
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
return Collections.emptyList();
|
||||
@@ -745,7 +745,7 @@ public class CotisationService {
|
||||
Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
List<Cotisation> cotisations = cotisationRepository.findEnAttenteByOrganisationIdIn(orgIds);
|
||||
log.info("Cotisations en attente (admin): {} pour {} organisations", cotisations.size(), orgIds.size());
|
||||
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
||||
return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
|
||||
if (membreConnecte == null) {
|
||||
@@ -758,6 +758,7 @@ public class CotisationService {
|
||||
List<Cotisation> cotisations = cotisationRepository.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT c FROM Cotisation c " +
|
||||
"LEFT JOIN FETCH c.membre LEFT JOIN FETCH c.organisation " +
|
||||
"WHERE c.membre.id = :membreId " +
|
||||
"AND c.statut = 'EN_ATTENTE' " +
|
||||
"AND EXTRACT(YEAR FROM c.dateEcheance) = :annee " +
|
||||
@@ -769,7 +770,7 @@ public class CotisationService {
|
||||
log.info("Cotisations en attente trouvées: {} pour le membre {}",
|
||||
cotisations.size(), membreConnecte.getNumeroMembre());
|
||||
return cotisations.stream()
|
||||
.map(this::convertToSummaryResponse)
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import dev.lions.unionflow.server.mapper.DemandeAideMapper;
|
||||
import dev.lions.unionflow.server.repository.DemandeAideRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import io.quarkus.panache.common.Page;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -403,9 +405,19 @@ public class DemandeAideService {
|
||||
cacheDemandesRecentes.keySet().retainAll(cacheTimestamps.keySet());
|
||||
}
|
||||
|
||||
/** Charge toutes les demandes depuis la base et les mappe en DTO. */
|
||||
/**
|
||||
* Charge les demandes depuis la base avec une limite de 1000 enregistrements.
|
||||
* Log un avertissement si le résultat dépasse 500 éléments pour anticiper les risques OOM.
|
||||
*/
|
||||
private List<DemandeAideResponse> chargerToutesLesDemandesDepuisBDD() {
|
||||
List<DemandeAide> entities = demandeAideRepository.listAll();
|
||||
int limite = 1000;
|
||||
List<DemandeAide> entities = demandeAideRepository.findAll(
|
||||
Page.ofSize(limite),
|
||||
Sort.by("dateDemande", Sort.Direction.Descending)
|
||||
);
|
||||
if (entities.size() > 500) {
|
||||
LOG.warnf("chargerToutesLesDemandesDepuisBDD : %d demandes chargées en mémoire — risque OOM si la volumétrie continue de croître", entities.size());
|
||||
}
|
||||
return entities.stream()
|
||||
.map(demandeAideMapper::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -122,6 +122,19 @@ public class DocumentService {
|
||||
return convertToResponse(pieceJointe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les documents de l'utilisateur connecté
|
||||
*
|
||||
* @return Liste des documents créés par l'utilisateur connecté
|
||||
*/
|
||||
public List<DocumentResponse> listerMesDocuments() {
|
||||
String email = keycloakService.getCurrentUserEmail();
|
||||
LOG.infof("Listing des documents pour l'utilisateur: %s", email);
|
||||
return documentRepository.findByCreePar(email).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les pièces jointes d'un document
|
||||
*
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest;
|
||||
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service de cycle de vie des membres au sein d'une organisation.
|
||||
*
|
||||
* <p>Gère les transitions de statut automatiques et manuelles :
|
||||
* <ul>
|
||||
* <li>Invitation → EN_ATTENTE_VALIDATION (après acceptation)</li>
|
||||
* <li>EN_ATTENTE_VALIDATION → ACTIF (après validation admin)</li>
|
||||
* <li>ACTIF → SUSPENDU (suspension manuelle ou automatique)</li>
|
||||
* <li>INVITE → expiré (si la date d'expiration est dépassée)</li>
|
||||
* <li>ACTIF → ARCHIVE (archivage)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Les tâches automatiques sont déclenchées par {@link MemberLifecycleScheduler}.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MemberLifecycleService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MemberLifecycleService.class);
|
||||
|
||||
/** Durée de validité par défaut d'une invitation (7 jours). */
|
||||
public static final int INVITATION_EXPIRY_DAYS = 7;
|
||||
|
||||
/** Délai de rappel avant expiration (24h avant). */
|
||||
public static final int INVITATION_REMINDER_HOURS = 24;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrgRepository;
|
||||
|
||||
@Inject
|
||||
NotificationService notificationService;
|
||||
|
||||
// =========================================================================
|
||||
// Transitions manuelles
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Active un membre dans une organisation (transition → ACTIF).
|
||||
*
|
||||
* @param membreOrgId UUID du lien membre-organisation
|
||||
* @param adminId UUID de l'administrateur qui valide
|
||||
* @param motif Motif optionnel de l'activation
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation activerMembre(UUID membreOrgId, UUID adminId, String motif) {
|
||||
MembreOrganisation lien = charger(membreOrgId);
|
||||
verifierTransitionAutorisee(lien.getStatutMembre(),
|
||||
StatutMembre.EN_ATTENTE_VALIDATION, StatutMembre.INVITE, StatutMembre.SUSPENDU);
|
||||
|
||||
lien.setStatutMembre(StatutMembre.ACTIF);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setMotifStatut(motif != null ? motif : "Validation par l'administrateur");
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
// Mettre à jour le compteur de membres de l'organisation
|
||||
lien.getOrganisation().ajouterMembre();
|
||||
|
||||
LOG.infof("Membre %s activé dans organisation %s par admin %s",
|
||||
lien.getMembre().getId(), lien.getOrganisation().getId(), adminId);
|
||||
|
||||
// Notifier le membre
|
||||
envoyerNotification(lien.getMembre(), "Adhésion activée",
|
||||
"Votre adhésion à " + lien.getOrganisation().getNom() + " est maintenant active.");
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend un membre dans une organisation (ACTIF → SUSPENDU).
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation suspendreMembre(UUID membreOrgId, UUID adminId, String motif) {
|
||||
MembreOrganisation lien = charger(membreOrgId);
|
||||
verifierTransitionAutorisee(lien.getStatutMembre(), StatutMembre.ACTIF);
|
||||
|
||||
lien.setStatutMembre(StatutMembre.SUSPENDU);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setMotifStatut(motif != null ? motif : "Suspension par l'administrateur");
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
LOG.infof("Membre %s suspendu dans organisation %s", lien.getMembre().getId(),
|
||||
lien.getOrganisation().getId());
|
||||
|
||||
envoyerNotification(lien.getMembre(), "Adhésion suspendue",
|
||||
"Votre adhésion à " + lien.getOrganisation().getNom() + " a été suspendue. Motif : " + motif);
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
/**
|
||||
* Radie un membre d'une organisation (→ RADIE).
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation radierMembre(UUID membreOrgId, UUID adminId, String motif) {
|
||||
MembreOrganisation lien = charger(membreOrgId);
|
||||
|
||||
lien.setStatutMembre(StatutMembre.RADIE);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setMotifStatut(motif != null ? motif : "Radiation par l'administrateur");
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
// Mettre à jour le compteur de membres de l'organisation
|
||||
lien.getOrganisation().retirerMembre();
|
||||
|
||||
LOG.infof("Membre %s radié de l'organisation %s. Motif: %s",
|
||||
lien.getMembre().getId(), lien.getOrganisation().getId(), motif);
|
||||
|
||||
envoyerNotification(lien.getMembre(), "Adhésion radiée",
|
||||
"Votre adhésion à " + lien.getOrganisation().getNom() + " a été radiée.");
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive un membre (→ ARCHIVE) sans supprimer l'historique.
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation archiverMembre(UUID membreOrgId, String motif) {
|
||||
MembreOrganisation lien = charger(membreOrgId);
|
||||
|
||||
lien.setStatutMembre(StatutMembre.ARCHIVE);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setMotifArchivage(motif);
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
// Mettre à jour le compteur de membres de l'organisation
|
||||
lien.getOrganisation().retirerMembre();
|
||||
|
||||
LOG.infof("Membre %s archivé dans l'organisation %s", lien.getMembre().getId(),
|
||||
lien.getOrganisation().getId());
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une invitation (statut INVITE) pour un nouveau membre.
|
||||
*
|
||||
* @param membre Le membre à inviter (doit exister en base)
|
||||
* @param organisation L'organisation cible
|
||||
* @param adminId L'administrateur qui invite
|
||||
* @param roleOrg Rôle proposé dans l'organisation (optionnel)
|
||||
* @return le lien MembreOrganisation créé avec le token d'invitation
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation inviterMembre(
|
||||
dev.lions.unionflow.server.entity.Membre membre,
|
||||
dev.lions.unionflow.server.entity.Organisation organisation,
|
||||
UUID adminId,
|
||||
String roleOrg) {
|
||||
|
||||
// Vérifier s'il n'existe pas déjà un lien actif
|
||||
membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), organisation.getId())
|
||||
.ifPresent(existing -> {
|
||||
throw new IllegalStateException("Le membre est déjà lié à cette organisation.");
|
||||
});
|
||||
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
MembreOrganisation lien = MembreOrganisation.builder()
|
||||
.membre(membre)
|
||||
.organisation(organisation)
|
||||
.statutMembre(StatutMembre.INVITE)
|
||||
.dateInvitation(now)
|
||||
.dateExpirationInvitation(now.plusDays(INVITATION_EXPIRY_DAYS))
|
||||
.tokenInvitation(token)
|
||||
.invitePar(adminId)
|
||||
.roleOrg(roleOrg)
|
||||
.build();
|
||||
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
LOG.infof("Invitation créée pour membre %s dans organisation %s (expire: %s)",
|
||||
membre.getId(), organisation.getId(), lien.getDateExpirationInvitation());
|
||||
|
||||
// Notifier le membre
|
||||
envoyerNotification(membre, "Invitation à rejoindre " + organisation.getNom(),
|
||||
"Vous avez été invité à rejoindre " + organisation.getNom() +
|
||||
". Votre invitation expire dans " + INVITATION_EXPIRY_DAYS + " jours.");
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepte une invitation via son token (INVITE → EN_ATTENTE_VALIDATION).
|
||||
*/
|
||||
@Transactional
|
||||
public MembreOrganisation accepterInvitation(String token) {
|
||||
MembreOrganisation lien = membreOrgRepository
|
||||
.find("tokenInvitation = ?1 and statutMembre = ?2", token, StatutMembre.INVITE)
|
||||
.firstResultOptional()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invitation introuvable ou déjà utilisée."));
|
||||
|
||||
if (lien.getDateExpirationInvitation() != null &&
|
||||
lien.getDateExpirationInvitation().isBefore(LocalDateTime.now())) {
|
||||
throw new IllegalStateException("Cette invitation a expiré.");
|
||||
}
|
||||
|
||||
lien.setStatutMembre(StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setTokenInvitation(null); // Invalider le token après usage
|
||||
membreOrgRepository.persist(lien);
|
||||
|
||||
LOG.infof("Invitation acceptée par membre %s dans organisation %s",
|
||||
lien.getMembre().getId(), lien.getOrganisation().getId());
|
||||
|
||||
return lien;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Traitements automatiques (appelés par MemberLifecycleScheduler)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Expire les invitations dont la date limite est dépassée.
|
||||
* Les membres INVITE passent au statut RADIE.
|
||||
*
|
||||
* @return nombre d'invitations expirées
|
||||
*/
|
||||
@Transactional
|
||||
public int expirerInvitations() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<MembreOrganisation> expiredInvitations = membreOrgRepository.findInvitationsExpirees(now);
|
||||
|
||||
int count = 0;
|
||||
for (MembreOrganisation lien : expiredInvitations) {
|
||||
lien.setStatutMembre(StatutMembre.RADIE);
|
||||
lien.setDateChangementStatut(LocalDate.now());
|
||||
lien.setMotifStatut("Invitation expirée sans réponse");
|
||||
membreOrgRepository.persist(lien);
|
||||
count++;
|
||||
|
||||
LOG.infof("Invitation expirée : membre %s dans org %s",
|
||||
lien.getMembre().getId(), lien.getOrganisation().getId());
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
LOG.infof("Expiration des invitations : %d invitation(s) expirée(s)", count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie des rappels pour les invitations qui expirent dans les prochaines 24h.
|
||||
*
|
||||
* @return nombre de rappels envoyés
|
||||
*/
|
||||
@Transactional
|
||||
public int envoyerRappelsInvitation() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime dans24h = now.plusHours(INVITATION_REMINDER_HOURS);
|
||||
List<MembreOrganisation> aRappeler =
|
||||
membreOrgRepository.findInvitationsExpirantBientot(now, dans24h);
|
||||
|
||||
int count = 0;
|
||||
for (MembreOrganisation lien : aRappeler) {
|
||||
envoyerNotification(lien.getMembre(),
|
||||
"Rappel : votre invitation expire bientôt",
|
||||
"Votre invitation à rejoindre " + lien.getOrganisation().getNom() +
|
||||
" expire dans moins de 24h. Acceptez-la maintenant.");
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
LOG.infof("Rappels d'invitation : %d rappel(s) envoyé(s)", count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers privés
|
||||
// =========================================================================
|
||||
|
||||
private MembreOrganisation charger(UUID membreOrgId) {
|
||||
return membreOrgRepository.findByIdOptional(membreOrgId)
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Lien membre-organisation introuvable : " + membreOrgId));
|
||||
}
|
||||
|
||||
private void verifierTransitionAutorisee(StatutMembre current, StatutMembre... autorises) {
|
||||
for (StatutMembre autorise : autorises) {
|
||||
if (autorise.equals(current)) return;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Transition non autorisée depuis le statut " + current);
|
||||
}
|
||||
|
||||
private void envoyerNotification(Membre membre, String titre, String corps) {
|
||||
try {
|
||||
if (notificationService != null) {
|
||||
CreateNotificationRequest req = CreateNotificationRequest.builder()
|
||||
.typeNotification("SYSTEME")
|
||||
.priorite("NORMALE")
|
||||
.sujet(titre)
|
||||
.corps(corps)
|
||||
.membreId(membre.getId())
|
||||
.build();
|
||||
notificationService.creerNotification(req);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible d'envoyer la notification à %s : %s", membre.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,19 @@ import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
import io.quarkus.oidc.client.NamedOidcClient;
|
||||
import io.quarkus.oidc.client.OidcClient;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -60,6 +67,14 @@ public class MembreKeycloakSyncService {
|
||||
private static final Logger LOGGER = Logger.getLogger(MembreKeycloakSyncService.class.getName());
|
||||
private static final String DEFAULT_REALM = "unionflow";
|
||||
|
||||
/** URL du serveur Keycloak. Ex : https://security.lions.dev/realms/unionflow */
|
||||
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
|
||||
String oidcAuthServerUrl;
|
||||
|
||||
@Inject
|
||||
@NamedOidcClient("admin-service")
|
||||
OidcClient adminOidcClient;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@@ -518,7 +533,15 @@ public class MembreKeycloakSyncService {
|
||||
.temporary(false)
|
||||
.build();
|
||||
|
||||
userServiceClient.resetPassword(keycloakUserId, DEFAULT_REALM, resetRequest);
|
||||
// Tentative via lions-user-manager ; fallback sur l'API Admin Keycloak directe si 403
|
||||
try {
|
||||
userServiceClient.resetPassword(keycloakUserId, DEFAULT_REALM, resetRequest);
|
||||
} catch (jakarta.ws.rs.ForbiddenException | jakarta.ws.rs.ServiceUnavailableException e) {
|
||||
LOGGER.warning("lions-user-manager reset-password échoué (" + e.getMessage()
|
||||
+ "), fallback sur API Admin Keycloak directe.");
|
||||
changerMotDePasseDirectKeycloak(membre.getId(), nouveauMotDePasse);
|
||||
return; // changerMotDePasseDirectKeycloak persiste déjà les flags
|
||||
}
|
||||
|
||||
membre.setPremiereConnexion(false);
|
||||
// Auto-activation : le membre prouve son identité en changeant son mot de passe temporaire
|
||||
@@ -541,6 +564,75 @@ public class MembreKeycloakSyncService {
|
||||
LOGGER.info("Mot de passe premier login changé pour: " + membre.getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Change le mot de passe d'un membre en appelant directement l'API Admin Keycloak.
|
||||
* Bypass lions-user-manager (évite les erreurs 403 de service account).
|
||||
*
|
||||
* @param membreId UUID du membre UnionFlow
|
||||
* @param nouveauMotDePasse Nouveau mot de passe (en clair, transmis en HTTPS)
|
||||
*/
|
||||
@Transactional
|
||||
public void changerMotDePasseDirectKeycloak(UUID membreId, String nouveauMotDePasse) {
|
||||
LOGGER.info("Changement de mot de passe (direct Keycloak) pour membre ID: " + membreId);
|
||||
|
||||
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Membre non trouvé: " + membreId));
|
||||
|
||||
if (membre.getKeycloakId() == null) {
|
||||
throw new IllegalStateException("Le membre n'a pas de compte Keycloak: " + membre.getEmail());
|
||||
}
|
||||
|
||||
String keycloakUserId = membre.getKeycloakId().toString();
|
||||
|
||||
// Obtenir le token admin via OIDC client credentials
|
||||
String adminToken;
|
||||
try {
|
||||
adminToken = adminOidcClient.getTokens().await().indefinitely().getAccessToken();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Impossible d'obtenir le token admin Keycloak: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Dériver l'URL Admin Keycloak depuis l'URL OIDC
|
||||
// Ex: https://security.lions.dev/realms/unionflow → https://security.lions.dev/admin/realms/unionflow
|
||||
String adminUrl = oidcAuthServerUrl.replace("/realms/", "/admin/realms/")
|
||||
+ "/users/" + keycloakUserId + "/reset-password";
|
||||
|
||||
String body = String.format(
|
||||
"{\"type\":\"password\",\"value\":\"%s\",\"temporary\":false}",
|
||||
nouveauMotDePasse.replace("\"", "\\\""));
|
||||
|
||||
try {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(adminUrl))
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.header("Content-Type", "application/json")
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 204 && response.statusCode() != 200) {
|
||||
throw new RuntimeException("Keycloak Admin API retourné " + response.statusCode()
|
||||
+ ": " + response.body());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Erreur lors de la réinitialisation du mot de passe Keycloak: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Mettre à jour les flags
|
||||
membre.setPremiereConnexion(false);
|
||||
if ("EN_ATTENTE_VALIDATION".equals(membre.getStatutCompte())) {
|
||||
membre.setStatutCompte("ACTIF");
|
||||
membre.setActif(true);
|
||||
}
|
||||
membreRepository.persist(membre);
|
||||
|
||||
LOGGER.info("Mot de passe changé (direct Keycloak) pour: " + membre.getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque le premier login comme complété.
|
||||
*
|
||||
|
||||
@@ -278,6 +278,11 @@ public class MembreService {
|
||||
return membreRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
/** Trouve un membre par son numéro de membre (ex: MBR-0001) */
|
||||
public Optional<Membre> trouverParNumeroMembre(String numeroMembre) {
|
||||
return membreRepository.findByNumeroMembre(numeroMembre);
|
||||
}
|
||||
|
||||
/** Liste tous les membres actifs */
|
||||
public List<Membre> listerMembresActifs() {
|
||||
return membreRepository.findAllActifs();
|
||||
@@ -523,12 +528,14 @@ public class MembreService {
|
||||
|
||||
UUID organisationId = null;
|
||||
String organisationNom = null;
|
||||
java.time.LocalDate dateAdhesion = null;
|
||||
if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
|
||||
dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
|
||||
if (mo.getOrganisation() != null) {
|
||||
organisationId = mo.getOrganisation().getId();
|
||||
organisationNom = mo.getOrganisation().getNom();
|
||||
}
|
||||
dateAdhesion = mo.getDateAdhesion();
|
||||
}
|
||||
|
||||
return new MembreSummaryResponse(
|
||||
@@ -545,7 +552,8 @@ public class MembreService {
|
||||
membre.getActif(),
|
||||
rolesNames,
|
||||
organisationId,
|
||||
organisationNom);
|
||||
organisationNom,
|
||||
dateAdhesion);
|
||||
}
|
||||
|
||||
/** Convertit un CreateMembreRequest en entité Membre */
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service de gestion des modules actifs par organisation.
|
||||
*
|
||||
* <p>Architecture Option C — les modules actifs sont déterminés par le TYPE
|
||||
* d'organisation, pas par le plan tarifaire. Le plan impacte uniquement la
|
||||
* profondeur fonctionnelle (reporting, API, fédération).
|
||||
*
|
||||
* <p>Les modules sont regroupés en deux catégories :
|
||||
* <ul>
|
||||
* <li>MODULES_COMMUNS — accessibles à tous les types d'org</li>
|
||||
* <li>Modules métier — spécifiques au type d'org (TONTINE, CREDIT, etc.)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class OrganisationModuleService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(OrganisationModuleService.class);
|
||||
|
||||
/** Modules présents sur toutes les organisations quelle que soit leur nature. */
|
||||
public static final Set<String> MODULES_COMMUNS = Set.of(
|
||||
"MEMBRES",
|
||||
"COTISATIONS",
|
||||
"EVENEMENTS",
|
||||
"COMMUNICATION",
|
||||
"DOCUMENTS",
|
||||
"NOTIFICATION",
|
||||
"AIDE"
|
||||
);
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
/**
|
||||
* Retourne l'ensemble des modules actifs pour une organisation donnée.
|
||||
* Combine les modules communs avec les modules métier du type d'org.
|
||||
*/
|
||||
public Set<String> getModulesActifs(UUID organisationId) {
|
||||
Optional<Organisation> opt = organisationRepository.findByIdOptional(organisationId);
|
||||
if (opt.isEmpty()) {
|
||||
LOG.warnf("Organisation introuvable : %s", organisationId);
|
||||
return MODULES_COMMUNS;
|
||||
}
|
||||
return getModulesActifs(opt.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'ensemble des modules actifs pour une organisation.
|
||||
*/
|
||||
public Set<String> getModulesActifs(Organisation organisation) {
|
||||
Set<String> modules = new LinkedHashSet<>(MODULES_COMMUNS);
|
||||
|
||||
// 1. Modules issus du champ modulesActifs persisté (calculé depuis types_reference en V18)
|
||||
String modulesActifsCsv = organisation.getModulesActifs();
|
||||
if (modulesActifsCsv != null && !modulesActifsCsv.isBlank()) {
|
||||
Arrays.stream(modulesActifsCsv.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.forEach(modules::add);
|
||||
return modules;
|
||||
}
|
||||
|
||||
// 2. Fallback : déduction depuis le typeOrganisation si modulesActifs non renseigné
|
||||
modules.addAll(getModulesParType(organisation.getTypeOrganisation()));
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un module spécifique est actif pour une organisation.
|
||||
*/
|
||||
public boolean isModuleActif(UUID organisationId, String module) {
|
||||
return getModulesActifs(organisationId).contains(module.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les modules métier associés à un type d'organisation.
|
||||
* Utilisé en fallback si la colonne modules_actifs n'est pas peuplée.
|
||||
*/
|
||||
public Set<String> getModulesParType(String typeOrganisation) {
|
||||
if (typeOrganisation == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> modules = new LinkedHashSet<>();
|
||||
switch (typeOrganisation.toUpperCase()) {
|
||||
case "TONTINE" -> {
|
||||
modules.add("TONTINE");
|
||||
modules.add("FINANCE");
|
||||
}
|
||||
case "MUTUELLE_EPARGNE" -> {
|
||||
modules.add("EPARGNE");
|
||||
modules.add("FINANCE");
|
||||
modules.add("LCB_FT");
|
||||
}
|
||||
case "MUTUELLE_CREDIT" -> {
|
||||
modules.add("EPARGNE");
|
||||
modules.add("CREDIT");
|
||||
modules.add("FINANCE");
|
||||
modules.add("LCB_FT");
|
||||
}
|
||||
case "COOPERATIVE" -> {
|
||||
modules.add("AGRICULTURE");
|
||||
modules.add("FINANCE");
|
||||
}
|
||||
case "ONG", "FONDATION" -> {
|
||||
modules.add("PROJETS_ONG");
|
||||
modules.add("COLLECTE_FONDS");
|
||||
modules.add("FINANCE");
|
||||
}
|
||||
case "EGLISE", "GROUPE_PRIERE" -> {
|
||||
modules.add("CULTE_DONS");
|
||||
}
|
||||
case "SYNDICAT", "ORDRE_PROFESSIONNEL", "FEDERATION" -> {
|
||||
modules.add("VOTES");
|
||||
modules.add("REGISTRE_AGREMENT");
|
||||
}
|
||||
case "GIE" -> {
|
||||
modules.add("FINANCE");
|
||||
}
|
||||
case "ASSOCIATION", "CLUB_SERVICE", "CLUB_SPORTIF", "CLUB_CULTUREL" -> {
|
||||
modules.add("VOTES");
|
||||
}
|
||||
default -> LOG.debugf("Type d''organisation non reconnu pour module mapping : %s", typeOrganisation);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des modules actifs sous forme de tableau JSON-friendly.
|
||||
* Utilisé par l'endpoint /api/organisations/{id}/modules-actifs
|
||||
*/
|
||||
public ModulesActifsResponse getModulesActifsResponse(UUID organisationId) {
|
||||
Optional<Organisation> opt = organisationRepository.findByIdOptional(organisationId);
|
||||
if (opt.isEmpty()) {
|
||||
return new ModulesActifsResponse(organisationId, Collections.emptySet(), "UNKNOWN");
|
||||
}
|
||||
Organisation org = opt.get();
|
||||
Set<String> modules = getModulesActifs(org);
|
||||
return new ModulesActifsResponse(organisationId, modules, org.getTypeOrganisation());
|
||||
}
|
||||
|
||||
/** DTO de réponse pour l'endpoint modules-actifs. */
|
||||
public record ModulesActifsResponse(UUID organisationId, Set<String> modules, String typeOrganisation) {}
|
||||
}
|
||||
@@ -333,7 +333,11 @@ public class PaiementService {
|
||||
.build();
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
String successUrl = base + "/api/wave-redirect/success?ref=" + intention.getId();
|
||||
// Web (sans numéro de téléphone) → page HTML de confirmation ; Mobile → deep link app
|
||||
boolean isWebContext = request.numeroTelephone() == null || request.numeroTelephone().isBlank();
|
||||
String successUrl = base + (isWebContext
|
||||
? "/api/wave-redirect/web-success?ref=" + intention.getId()
|
||||
: "/api/wave-redirect/success?ref=" + intention.getId());
|
||||
String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
||||
String clientRef = intention.getId().toString();
|
||||
// XOF : montant entier, pas de décimales (spec Wave)
|
||||
@@ -393,6 +397,113 @@ public class PaiementService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'une IntentionPaiement Wave.
|
||||
* Si la session Wave est complétée (paiement réussi), réconcilie automatiquement
|
||||
* la cotisation (marque PAYEE) et met à jour l'intention (COMPLETEE).
|
||||
* Appelé en polling depuis le web toutes les 3 secondes.
|
||||
*/
|
||||
@Transactional
|
||||
public dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse verifierStatutIntention(UUID intentionId) {
|
||||
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
|
||||
if (intention == null) {
|
||||
throw new NotFoundException("IntentionPaiement non trouvée: " + intentionId);
|
||||
}
|
||||
|
||||
// Déjà terminée — retourner immédiatement
|
||||
if (intention.isCompletee()) {
|
||||
return buildStatutResponse(intention, "Paiement confirmé !");
|
||||
}
|
||||
if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut())
|
||||
|| StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) {
|
||||
return buildStatutResponse(intention, "Paiement " + intention.getStatut().name().toLowerCase());
|
||||
}
|
||||
|
||||
// Session expirée côté UnionFlow (30 min)
|
||||
if (intention.isExpiree()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session expirée, veuillez recommencer");
|
||||
}
|
||||
|
||||
// Vérifier le statut côté Wave si session connue
|
||||
if (intention.getWaveCheckoutSessionId() != null) {
|
||||
try {
|
||||
WaveCheckoutService.WaveSessionStatusResponse waveStatus =
|
||||
waveCheckoutService.getSession(intention.getWaveCheckoutSessionId());
|
||||
|
||||
if (waveStatus.isSucceeded()) {
|
||||
completerIntention(intention, waveStatus.transactionId);
|
||||
return buildStatutResponse(intention, "Paiement confirmé !");
|
||||
} else if (waveStatus.isExpired()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session Wave expirée");
|
||||
}
|
||||
} catch (WaveCheckoutService.WaveCheckoutException e) {
|
||||
LOG.warnf(e, "Impossible de vérifier la session Wave %s — retry au prochain poll",
|
||||
intention.getWaveCheckoutSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
return buildStatutResponse(intention, "En attente de confirmation Wave...");
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'IntentionPaiement COMPLETEE et réconcilie les cotisations cibles (PAYEE).
|
||||
* Utilisé par le polling web ET par WaveRedirectResource lors du redirect success.
|
||||
*/
|
||||
@Transactional
|
||||
public void completerIntention(IntentionPaiement intention, String waveTransactionId) {
|
||||
if (intention.isCompletee()) return; // idempotent
|
||||
|
||||
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
|
||||
intention.setDateCompletion(java.time.LocalDateTime.now());
|
||||
if (waveTransactionId != null) intention.setWaveTransactionId(waveTransactionId);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
// Réconcilier les cotisations listées dans objetsCibles
|
||||
String objetsCibles = intention.getObjetsCibles();
|
||||
if (objetsCibles == null || objetsCibles.isBlank()) return;
|
||||
|
||||
try {
|
||||
com.fasterxml.jackson.databind.JsonNode arr =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper().readTree(objetsCibles);
|
||||
if (!arr.isArray()) return;
|
||||
for (com.fasterxml.jackson.databind.JsonNode node : arr) {
|
||||
if (!"COTISATION".equals(node.path("type").asText())) continue;
|
||||
UUID cotisationId = UUID.fromString(node.get("id").asText());
|
||||
java.math.BigDecimal montant = node.has("montant")
|
||||
? new java.math.BigDecimal(node.get("montant").asText())
|
||||
: intention.getMontantTotal();
|
||||
|
||||
Cotisation cotisation = paiementRepository.getEntityManager().find(Cotisation.class, cotisationId);
|
||||
if (cotisation == null) continue;
|
||||
|
||||
cotisation.setMontantPaye(montant);
|
||||
cotisation.setStatut("PAYEE");
|
||||
cotisation.setDatePaiement(java.time.LocalDateTime.now());
|
||||
paiementRepository.getEntityManager().merge(cotisation);
|
||||
LOG.infof("Cotisation %s marquée PAYEE — Wave txn %s", cotisationId, waveTransactionId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur réconciliation cotisations pour intention %s", intention.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse buildStatutResponse(
|
||||
IntentionPaiement intention, String message) {
|
||||
return dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse.builder()
|
||||
.intentionId(intention.getId())
|
||||
.statut(intention.getStatut().name())
|
||||
.waveLaunchUrl(intention.getWaveLaunchUrl())
|
||||
.waveCheckoutSessionId(intention.getWaveCheckoutSessionId())
|
||||
.waveTransactionId(intention.getWaveTransactionId())
|
||||
.montant(intention.getMontantTotal())
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Format E.164 pour Wave (ex: 771234567 -> +225771234567). */
|
||||
private static String toE164(String numeroTelephone) {
|
||||
if (numeroTelephone == null || numeroTelephone.isBlank()) return null;
|
||||
|
||||
@@ -306,6 +306,40 @@ public class SouscriptionService {
|
||||
|
||||
// ── Validation SuperAdmin ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liste toutes les souscriptions (SuperAdmin), avec filtre optionnel par organisation.
|
||||
*/
|
||||
public List<SouscriptionStatutResponse> listerToutes(UUID organisationId, int page, int size) {
|
||||
if (organisationId != null) {
|
||||
return souscriptionRepo
|
||||
.find("organisation.id = ?1 order by dateCreation desc", organisationId)
|
||||
.page(page, size)
|
||||
.list()
|
||||
.stream()
|
||||
.map(s -> toStatutResponse(s, null))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return souscriptionRepo
|
||||
.findAll()
|
||||
.page(page, size)
|
||||
.list()
|
||||
.stream()
|
||||
.map(s -> toStatutResponse(s, null))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la souscription active d'une organisation (SuperAdmin).
|
||||
*/
|
||||
public SouscriptionStatutResponse obtenirActiveParOrganisation(UUID organisationId) {
|
||||
return souscriptionRepo
|
||||
.find("organisation.id = ?1 and statut = ?2 order by dateCreation desc",
|
||||
organisationId, dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription.ACTIVE)
|
||||
.firstResultOptional()
|
||||
.map(s -> toStatutResponse(s, null))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les souscriptions en attente de validation SuperAdmin.
|
||||
*/
|
||||
@@ -499,6 +533,7 @@ public class SouscriptionService {
|
||||
r.setDateFin(s.getDateFin());
|
||||
r.setDateValidation(s.getDateValidation());
|
||||
r.setCommentaireRejet(s.getCommentaireRejet());
|
||||
r.setStatut(s.getStatut() != null ? s.getStatut().name() : null);
|
||||
if (s.getOrganisation() != null) {
|
||||
r.setOrganisationId(s.getOrganisation().getId().toString());
|
||||
r.setOrganisationNom(s.getOrganisation().getNom());
|
||||
|
||||
@@ -58,6 +58,8 @@ public class SystemMetricsService {
|
||||
private final AtomicLong apiRequestsCount = new AtomicLong(0);
|
||||
private final AtomicLong apiRequestsLastHour = new AtomicLong(0);
|
||||
private final AtomicLong apiRequestsToday = new AtomicLong(0);
|
||||
private final AtomicLong requestCount = new AtomicLong(0);
|
||||
private final AtomicLong totalResponseTime = new AtomicLong(0);
|
||||
private long startTimeMillis;
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@@ -283,11 +285,15 @@ public class SystemMetricsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nombre de sessions actives
|
||||
* Nombre de sessions actives (proxy : membres avec compte ACTIF)
|
||||
*/
|
||||
private Integer getActiveSessionsCount() {
|
||||
// TODO: Implémenter avec vrai système de sessions Keycloak
|
||||
return 0;
|
||||
try {
|
||||
return (int) membreRepository.count("statutCompte = 'ACTIF'");
|
||||
} catch (Exception e) {
|
||||
log.warn("Impossible de compter les membres actifs", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,11 +309,12 @@ public class SystemMetricsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Temps de réponse moyen API
|
||||
* Temps de réponse moyen API (basé sur les appels enregistrés via recordRequest)
|
||||
*/
|
||||
private Double getAverageResponseTime() {
|
||||
// TODO: Implémenter avec vrai système de métriques
|
||||
return 0.0;
|
||||
long count = requestCount.get();
|
||||
if (count == 0) return 0.0;
|
||||
return (double) totalResponseTime.get() / count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,4 +429,14 @@ public class SystemMetricsService {
|
||||
apiRequestsLastHour.incrementAndGet();
|
||||
apiRequestsToday.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistrer une requête avec son temps de réponse (en ms)
|
||||
* Permet le calcul du temps de réponse moyen via getAverageResponseTime()
|
||||
*/
|
||||
public void recordRequest(long responseTimeMs) {
|
||||
requestCount.incrementAndGet();
|
||||
totalResponseTime.addAndGet(responseTimeMs);
|
||||
incrementApiRequestCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,62 @@ public class WaveCheckoutService {
|
||||
return HexFormat.of().formatHex(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interroge l'état d'une session Wave Checkout (spec : GET /v1/checkout/sessions/:id).
|
||||
* Utilisé par le polling web pour détecter automatiquement la complétion du paiement.
|
||||
*
|
||||
* @param sessionId ID de session Wave (cos-xxx)
|
||||
* @return statut de la session (checkout_status, payment_status, transaction_id)
|
||||
*/
|
||||
public WaveSessionStatusResponse getSession(String sessionId) throws WaveCheckoutException {
|
||||
boolean useMock = mockEnabled || apiKey == null || apiKey.trim().isBlank();
|
||||
if (useMock) {
|
||||
// En mock, on ne peut pas vraiment vérifier — retourner EN_COURS (polling s'arrête via /web-success)
|
||||
LOG.warnf("Wave getSession en mode MOCK — session %s", sessionId);
|
||||
return new WaveSessionStatusResponse(sessionId, "open", "processing", null);
|
||||
}
|
||||
|
||||
String base = (baseUrl == null || baseUrl.endsWith("/")) ? baseUrl.replaceAll("/+$", "") : baseUrl;
|
||||
if (!base.endsWith("/v1")) base = base + "/v1";
|
||||
String url = base + "/checkout/sessions/" + sessionId;
|
||||
|
||||
try {
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.GET();
|
||||
|
||||
if (signingSecret != null && !signingSecret.trim().isBlank()) {
|
||||
String sig = computeWaveSignature(timestamp, "");
|
||||
requestBuilder.header("Wave-Signature", "t=" + timestamp + ",v1=" + sig);
|
||||
}
|
||||
|
||||
java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder().build();
|
||||
java.net.http.HttpResponse<String> response = client.send(
|
||||
requestBuilder.build(),
|
||||
java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
|
||||
if (response.statusCode() >= 400) {
|
||||
throw new WaveCheckoutException("Wave API: " + response.statusCode() + " " + response.body());
|
||||
}
|
||||
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
String checkoutStatus = root.has("checkout_status") ? root.get("checkout_status").asText() : null;
|
||||
String paymentStatus = root.has("payment_status") ? root.get("payment_status").asText() : null;
|
||||
String transactionId = root.has("transaction_id") ? root.get("transaction_id").asText() : null;
|
||||
return new WaveSessionStatusResponse(sessionId, checkoutStatus, paymentStatus, transactionId);
|
||||
|
||||
} catch (WaveCheckoutException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
throw new WaveCheckoutException("Erreur vérification session Wave: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getRedirectBaseUrl() {
|
||||
return (redirectBaseUrl == null || redirectBaseUrl.trim().isBlank()) ? "http://localhost:8080" : redirectBaseUrl.trim();
|
||||
}
|
||||
@@ -159,6 +215,31 @@ public class WaveCheckoutService {
|
||||
return new WaveCheckoutSessionResponse(mockId, successUrl);
|
||||
}
|
||||
|
||||
public static final class WaveSessionStatusResponse {
|
||||
public final String sessionId;
|
||||
/** "open" | "complete" | "expired" */
|
||||
public final String checkoutStatus;
|
||||
/** "processing" | "cancelled" | "succeeded" */
|
||||
public final String paymentStatus;
|
||||
/** ID transaction Wave (TCN...) — non-null si succeeded */
|
||||
public final String transactionId;
|
||||
|
||||
public WaveSessionStatusResponse(String sessionId, String checkoutStatus, String paymentStatus, String transactionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.checkoutStatus = checkoutStatus;
|
||||
this.paymentStatus = paymentStatus;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public boolean isSucceeded() {
|
||||
return "succeeded".equals(paymentStatus) && "complete".equals(checkoutStatus);
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return "expired".equals(checkoutStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WaveCheckoutSessionResponse {
|
||||
public final String id;
|
||||
public final String waveLaunchUrl;
|
||||
|
||||
@@ -26,8 +26,8 @@ quarkus.http.cors.origins=*
|
||||
quarkus.oidc.tenant-enabled=true
|
||||
quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow
|
||||
quarkus.oidc.client-id=unionflow-server
|
||||
# Validation audience : seuls les tokens destinés à unionflow-server sont acceptés
|
||||
# Nécessite un audience mapper dans Keycloak : unionflow-mobile client → scope → audience mapper → unionflow-server
|
||||
# Audience mapper configuré sur unionflow-client et unionflow-mobile dans Keycloak
|
||||
# → les tokens contiennent désormais "unionflow-server" dans le claim aud
|
||||
quarkus.oidc.token.audience=unionflow-server
|
||||
quarkus.oidc.credentials.secret=unionflow-secret-2025
|
||||
quarkus.oidc.tls.verification=none
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
-- ============================================================================
|
||||
-- V17 — Catégorisation des rôles + seed rôles fonctionnels et métier
|
||||
-- Architecture hybride RBAC : PLATEFORME + FONCTIONNEL + METIER
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Ajouter la colonne categorie
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS categorie VARCHAR(30);
|
||||
|
||||
-- 2. Taguer les rôles plateforme existants (seedés en V13)
|
||||
UPDATE roles
|
||||
SET categorie = 'PLATEFORME'
|
||||
WHERE code IN ('SUPERADMIN', 'ORGADMIN', 'MODERATOR', 'ACTIVEMEMBER', 'SIMPLEMEMBER', 'VISITOR')
|
||||
AND categorie IS NULL;
|
||||
|
||||
-- 3. Contrainte NOT NULL après mise à jour (valeur par défaut)
|
||||
ALTER TABLE roles ALTER COLUMN categorie SET DEFAULT 'FONCTIONNEL';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'roles' AND column_name = 'categorie') THEN
|
||||
UPDATE roles SET categorie = 'FONCTIONNEL' WHERE categorie IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. Index sur categorie
|
||||
CREATE INDEX IF NOT EXISTS idx_role_categorie ON roles (categorie);
|
||||
|
||||
-- ============================================================================
|
||||
-- Rôles FONCTIONNELS — communs à toutes les organisations
|
||||
-- ============================================================================
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'FONCTIONNEL', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('PRESIDENT', 'Président', 'Représentant légal de l''organisation', 15),
|
||||
('VICE_PRESIDENT', 'Vice-Président', 'Représentant légal adjoint', 16),
|
||||
('SECRETAIRE', 'Secrétaire', 'Gestion administrative et des archives', 25),
|
||||
('SECRETAIRE_ADJOINT', 'Secrétaire Adjoint', 'Assistant du secrétaire', 26),
|
||||
('TRESORIER', 'Trésorier', 'Gestion des finances et de la trésorerie', 25),
|
||||
('TRESORIER_ADJOINT', 'Trésorier Adjoint', 'Assistant du trésorier', 26),
|
||||
('COMMISSAIRE_COMPTES', 'Commissaire aux Comptes', 'Contrôle et audit interne des comptes', 30),
|
||||
('RESPONSABLE_MEMBRES', 'Responsable des Membres', 'Gestion des adhésions et des membres', 35),
|
||||
('RESPONSABLE_COMMUNICATION','Responsable Communication', 'Communication interne et externe', 35),
|
||||
('RESPONSABLE_EVENEMENTS', 'Responsable Évènements', 'Organisation des évènements et activités', 35),
|
||||
('RESPONSABLE_TECHNIQUE', 'Responsable Technique', 'Support et outils techniques', 35),
|
||||
('RESPONSABLE_SOCIAL', 'Responsable Social', 'Activités solidaires et entraide', 35)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- ============================================================================
|
||||
-- Rôles METIER — spécifiques par type d'organisation
|
||||
-- ============================================================================
|
||||
|
||||
-- Tontine
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('TONTINE_MANAGER', 'Responsable Tontine', 'Gestion des cycles, cotisations et rotations tontine', 20),
|
||||
('TONTINE_COLLECTOR', 'Collecteur Tontine', 'Collecte des cotisations périodiques de la tontine', 30)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Mutuelle / Épargne / Crédit
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('MUTUELLE_RESP', 'Responsable Mutuelle', 'Gestion des comptes épargne et demandes de crédit', 20),
|
||||
('CREDIT_ANALYSTE', 'Analyste Crédit', 'Analyse et instruction des dossiers de crédit', 25),
|
||||
('EPARGNE_MANAGER', 'Gestionnaire Épargne', 'Supervision des comptes et transactions épargne', 25)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Coopérative
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('COOP_RESP', 'Responsable Coopérative', 'Gestion des campagnes et productions agricoles', 20)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- ONG / Association
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('ONG_RESP', 'Responsable ONG', 'Pilotage des projets et programmes ONG', 20),
|
||||
('PROJET_MANAGER', 'Chef de Projet', 'Gestion et suivi d''un projet spécifique', 25),
|
||||
('DONATEUR', 'Donateur', 'Contributeur financier externe à l''ONG', 50)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Culte / Religieux
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('CULTE_RESP', 'Responsable Culte', 'Gestion des dons religieux et activités cultuelles', 20),
|
||||
('PASTEUR', 'Pasteur / Imam / Prêtre', 'Leader spirituel de l''organisation religieuse', 10),
|
||||
('DIACRE', 'Diacre / Responsable Groupe', 'Responsable d''un groupe ou d''une cellule', 30)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Vote / Gouvernance
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('VOTE_RESP', 'Responsable Vote', 'Administration des campagnes de vote et élections', 20),
|
||||
('SCRUTATEUR', 'Scrutateur', 'Dépouillement et comptage des votes', 30)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Collecte de fonds
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('COLLECTE_RESP', 'Responsable Collecte', 'Gestion des campagnes de collecte de fonds', 20)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
|
||||
-- Registre / Professionnel
|
||||
INSERT INTO roles (id, nom, code, libelle, description, niveau_hierarchique, type_role, categorie, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT
|
||||
gen_random_uuid(), v.code, v.code, v.libelle, v.description, v.niveau_hierarchique, 'SYSTEME', 'METIER', true,
|
||||
NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
('REGISTRE_RESP', 'Responsable Registre', 'Gestion des agréments et accréditations professionnelles', 20),
|
||||
('CONSULTANT', 'Consultant', 'Accès lecture étendu pour analyse et conseil', 45),
|
||||
('GESTIONNAIRE_RH', 'Gestionnaire RH', 'Gestion des ressources humaines de l''organisation', 30)
|
||||
) AS v(code, libelle, description, niveau_hierarchique)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE roles.code = v.code);
|
||||
@@ -0,0 +1,135 @@
|
||||
-- ============================================================================
|
||||
-- V18 — Colonne categorie_type sur organisations + seed des 17 types officiels
|
||||
-- Colonnes NOT NULL de TypeReference : domaine, code, libelle,
|
||||
-- ordre_affichage (default 0), est_defaut (default false), est_systeme (default true pour seed)
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Ajouter les colonnes sur organisations
|
||||
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS categorie_type VARCHAR(50);
|
||||
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS modules_actifs TEXT;
|
||||
|
||||
-- 2. Index
|
||||
CREATE INDEX IF NOT EXISTS idx_organisation_categorie ON organisations (categorie_type);
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. S'assurer que les colonnes nécessaires existent dans types_reference
|
||||
-- ============================================================================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'types_reference' AND column_name = 'categorie'
|
||||
) THEN
|
||||
ALTER TABLE types_reference ADD COLUMN categorie VARCHAR(50);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'types_reference' AND column_name = 'modules_requis'
|
||||
) THEN
|
||||
ALTER TABLE types_reference ADD COLUMN modules_requis TEXT;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'types_reference' AND column_name = 'ordre_affichage'
|
||||
) THEN
|
||||
ALTER TABLE types_reference ADD COLUMN ordre_affichage INTEGER NOT NULL DEFAULT 0;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'types_reference' AND column_name = 'est_defaut'
|
||||
) THEN
|
||||
ALTER TABLE types_reference ADD COLUMN est_defaut BOOLEAN NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'types_reference' AND column_name = 'est_systeme'
|
||||
) THEN
|
||||
ALTER TABLE types_reference ADD COLUMN est_systeme BOOLEAN NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. Seed des types officiels dans types_reference
|
||||
-- ============================================================================
|
||||
|
||||
-- Catégorie ASSOCIATIF
|
||||
INSERT INTO types_reference (id, domaine, libelle, code, categorie, modules_requis, actif, est_defaut, est_systeme, ordre_affichage, date_creation, date_modification, version)
|
||||
SELECT gen_random_uuid(), 'TYPE_ORGANISATION', v.libelle, v.code, v.categorie, v.modules_requis, true, false, true, 0, NOW(), NOW(), 0
|
||||
FROM (VALUES
|
||||
('Association Générale', 'ASSOCIATION', 'ASSOCIATIF', 'MEMBRES,COTISATIONS,EVENEMENTS,COMMUNICATION,DOCUMENTS,VOTES,AIDE'),
|
||||
('Club Service', 'CLUB_SERVICE', 'ASSOCIATIF', 'MEMBRES,COTISATIONS,EVENEMENTS,COMMUNICATION,DOCUMENTS,VOTES,PROJETS_SOLIDAIRES'),
|
||||
('Club Sportif', 'CLUB_SPORTIF', 'ASSOCIATIF', 'MEMBRES,COTISATIONS,EVENEMENTS,COMMUNICATION,DOCUMENTS'),
|
||||
('Club Culturel', 'CLUB_CULTUREL','ASSOCIATIF', 'MEMBRES,COTISATIONS,EVENEMENTS,COMMUNICATION,DOCUMENTS')
|
||||
) AS v(libelle, code, categorie, modules_requis)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM types_reference WHERE types_reference.code = v.code AND domaine = 'TYPE_ORGANISATION'
|
||||
);
|
||||
|
||||
-- Catégorie FINANCIER_SOLIDAIRE
|
||||
INSERT INTO types_reference (id, domaine, libelle, code, categorie, modules_requis, actif, est_defaut, est_systeme, ordre_affichage, date_creation, date_modification, version)
|
||||
SELECT gen_random_uuid(), 'TYPE_ORGANISATION', v.libelle, v.code, v.categorie, v.modules_requis, true, false, true, 0, NOW(), NOW(), 0
|
||||
FROM (VALUES
|
||||
('Tontine', 'TONTINE', 'FINANCIER_SOLIDAIRE', 'MEMBRES,COTISATIONS,TONTINE,COMMUNICATION,DOCUMENTS,FINANCE'),
|
||||
('Mutuelle d''Épargne', 'MUTUELLE_EPARGNE', 'FINANCIER_SOLIDAIRE', 'MEMBRES,COTISATIONS,EPARGNE,COMMUNICATION,DOCUMENTS,FINANCE,LCB_FT'),
|
||||
('Mutuelle de Crédit', 'MUTUELLE_CREDIT', 'FINANCIER_SOLIDAIRE', 'MEMBRES,COTISATIONS,EPARGNE,CREDIT,COMMUNICATION,DOCUMENTS,FINANCE,LCB_FT'),
|
||||
('Coopérative', 'COOPERATIVE', 'FINANCIER_SOLIDAIRE', 'MEMBRES,COTISATIONS,AGRICULTURE,COMMUNICATION,DOCUMENTS,FINANCE')
|
||||
) AS v(libelle, code, categorie, modules_requis)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM types_reference WHERE types_reference.code = v.code AND domaine = 'TYPE_ORGANISATION'
|
||||
);
|
||||
|
||||
-- Catégorie RELIGIEUX
|
||||
INSERT INTO types_reference (id, domaine, libelle, code, categorie, modules_requis, actif, est_defaut, est_systeme, ordre_affichage, date_creation, date_modification, version)
|
||||
SELECT gen_random_uuid(), 'TYPE_ORGANISATION', v.libelle, v.code, v.categorie, v.modules_requis, true, false, true, 0, NOW(), NOW(), 0
|
||||
FROM (VALUES
|
||||
('Église / Communauté Religieuse', 'EGLISE', 'RELIGIEUX', 'MEMBRES,COTISATIONS,CULTE_DONS,EVENEMENTS,COMMUNICATION,DOCUMENTS'),
|
||||
('Groupe de Prière / Cellule', 'GROUPE_PRIERE', 'RELIGIEUX', 'MEMBRES,EVENEMENTS,COMMUNICATION,DOCUMENTS')
|
||||
) AS v(libelle, code, categorie, modules_requis)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM types_reference WHERE types_reference.code = v.code AND domaine = 'TYPE_ORGANISATION'
|
||||
);
|
||||
|
||||
-- Catégorie PROFESSIONNEL
|
||||
INSERT INTO types_reference (id, domaine, libelle, code, categorie, modules_requis, actif, est_defaut, est_systeme, ordre_affichage, date_creation, date_modification, version)
|
||||
SELECT gen_random_uuid(), 'TYPE_ORGANISATION', v.libelle, v.code, v.categorie, v.modules_requis, true, false, true, 0, NOW(), NOW(), 0
|
||||
FROM (VALUES
|
||||
('ONG', 'ONG', 'PROFESSIONNEL', 'MEMBRES,COTISATIONS,PROJETS_ONG,COLLECTE_FONDS,COMMUNICATION,DOCUMENTS,FINANCE'),
|
||||
('Fondation', 'FONDATION', 'PROFESSIONNEL', 'MEMBRES,COLLECTE_FONDS,PROJETS_ONG,COMMUNICATION,DOCUMENTS,FINANCE'),
|
||||
('Syndicat', 'SYNDICAT', 'PROFESSIONNEL', 'MEMBRES,COTISATIONS,VOTES,COMMUNICATION,DOCUMENTS,AIDE'),
|
||||
('Ordre Professionnel / Chambre', 'ORDRE_PROFESSIONNEL', 'PROFESSIONNEL', 'MEMBRES,COTISATIONS,REGISTRE_AGREMENT,COMMUNICATION,DOCUMENTS,VOTES'),
|
||||
('GIE', 'GIE', 'PROFESSIONNEL', 'MEMBRES,COTISATIONS,FINANCE,COMMUNICATION,DOCUMENTS')
|
||||
) AS v(libelle, code, categorie, modules_requis)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM types_reference WHERE types_reference.code = v.code AND domaine = 'TYPE_ORGANISATION'
|
||||
);
|
||||
|
||||
-- Catégorie RESEAU_FEDERATION
|
||||
INSERT INTO types_reference (id, domaine, libelle, code, categorie, modules_requis, actif, est_defaut, est_systeme, ordre_affichage, date_creation, date_modification, version)
|
||||
SELECT gen_random_uuid(), 'TYPE_ORGANISATION', v.libelle, v.code, v.categorie, v.modules_requis, true, false, true, 0, NOW(), NOW(), 0
|
||||
FROM (VALUES
|
||||
('Fédération d''Organisations', 'FEDERATION', 'RESEAU_FEDERATION', 'MEMBRES,COTISATIONS,VOTES,COMMUNICATION,DOCUMENTS,FINANCE'),
|
||||
('Réseau / Plateforme', 'RESEAU', 'RESEAU_FEDERATION', 'MEMBRES,EVENEMENTS,COMMUNICATION,DOCUMENTS')
|
||||
) AS v(libelle, code, categorie, modules_requis)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM types_reference WHERE types_reference.code = v.code AND domaine = 'TYPE_ORGANISATION'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. Migrer les valeurs existantes dans organisations.categorie_type
|
||||
-- ============================================================================
|
||||
UPDATE organisations o
|
||||
SET categorie_type = tr.categorie,
|
||||
modules_actifs = tr.modules_requis
|
||||
FROM types_reference tr
|
||||
WHERE tr.code = o.type_organisation
|
||||
AND tr.domaine = 'TYPE_ORGANISATION'
|
||||
AND o.categorie_type IS NULL;
|
||||
|
||||
-- Valeur par défaut pour les organisations sans correspondance
|
||||
UPDATE organisations
|
||||
SET categorie_type = 'ASSOCIATIF'
|
||||
WHERE categorie_type IS NULL;
|
||||
@@ -0,0 +1,180 @@
|
||||
-- ============================================================================
|
||||
-- V19 — Formules Option C : plan commercial + features + statuts org
|
||||
-- Option C : prix = f(taille), modules = f(type org), features = f(plan)
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Ajouter les colonnes Option C sur formules_abonnement
|
||||
-- ============================================================================
|
||||
|
||||
-- Nom commercial affiché à l'utilisateur (MICRO / DECOUVERTE / ESSENTIEL / AVANCE / PROFESSIONNEL / ENTERPRISE)
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS plan_commercial VARCHAR(30);
|
||||
|
||||
-- Niveau de reporting inclus dans ce plan
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS niveau_reporting VARCHAR(20) DEFAULT 'BASIQUE';
|
||||
|
||||
-- Accès API REST (pour intégrations externes)
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS api_access BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- Accès module fédération (gestion multi-org hiérarchique)
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS federation_access BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- Support prioritaire
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS support_prioritaire BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- SLA garanti (ex: '99.5%', '99.9%')
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS sla_garanti VARCHAR(10);
|
||||
|
||||
-- Nombre max d'administrateurs (NULL = illimité)
|
||||
ALTER TABLE formules_abonnement ADD COLUMN IF NOT EXISTS max_admins INTEGER;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Mapper les 12 formules avec leur plan_commercial et features
|
||||
-- ============================================================================
|
||||
|
||||
-- PETITE + BASIC → MICRO
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'MICRO',
|
||||
niveau_reporting = 'BASIQUE',
|
||||
api_access = FALSE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.0%',
|
||||
max_admins = 2
|
||||
WHERE code = 'BASIC' AND plage = 'PETITE';
|
||||
|
||||
-- PETITE + STANDARD → DECOUVERTE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'DECOUVERTE',
|
||||
niveau_reporting = 'STANDARD',
|
||||
api_access = FALSE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.0%',
|
||||
max_admins = 5
|
||||
WHERE code = 'STANDARD' AND plage = 'PETITE';
|
||||
|
||||
-- PETITE + PREMIUM → ESSENTIEL
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ESSENTIEL',
|
||||
niveau_reporting = 'STANDARD',
|
||||
api_access = TRUE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.5%',
|
||||
max_admins = 10
|
||||
WHERE code = 'PREMIUM' AND plage = 'PETITE';
|
||||
|
||||
-- MOYENNE + BASIC → ESSENTIEL
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ESSENTIEL',
|
||||
niveau_reporting = 'STANDARD',
|
||||
api_access = FALSE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.5%',
|
||||
max_admins = 5
|
||||
WHERE code = 'BASIC' AND plage = 'MOYENNE';
|
||||
|
||||
-- MOYENNE + STANDARD → AVANCE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'AVANCE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.5%',
|
||||
max_admins = 15
|
||||
WHERE code = 'STANDARD' AND plage = 'MOYENNE';
|
||||
|
||||
-- MOYENNE + PREMIUM → PROFESSIONNEL
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'PROFESSIONNEL',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'PREMIUM' AND plage = 'MOYENNE';
|
||||
|
||||
-- GRANDE + BASIC → AVANCE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'AVANCE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = FALSE,
|
||||
sla_garanti = '99.5%',
|
||||
max_admins = 10
|
||||
WHERE code = 'BASIC' AND plage = 'GRANDE';
|
||||
|
||||
-- GRANDE + STANDARD → PROFESSIONNEL
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'PROFESSIONNEL',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = FALSE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'STANDARD' AND plage = 'GRANDE';
|
||||
|
||||
-- GRANDE + PREMIUM → ENTERPRISE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ENTERPRISE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = TRUE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'PREMIUM' AND plage = 'GRANDE';
|
||||
|
||||
-- TRES_GRANDE + BASIC → ENTERPRISE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ENTERPRISE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = TRUE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'BASIC' AND plage = 'TRES_GRANDE';
|
||||
|
||||
-- TRES_GRANDE + STANDARD → ENTERPRISE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ENTERPRISE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = TRUE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'STANDARD' AND plage = 'TRES_GRANDE';
|
||||
|
||||
-- TRES_GRANDE + PREMIUM → ENTERPRISE
|
||||
UPDATE formules_abonnement SET
|
||||
plan_commercial = 'ENTERPRISE',
|
||||
niveau_reporting = 'AVANCE',
|
||||
api_access = TRUE,
|
||||
federation_access = TRUE,
|
||||
support_prioritaire = TRUE,
|
||||
sla_garanti = '99.9%',
|
||||
max_admins = NULL
|
||||
WHERE code = 'PREMIUM' AND plage = 'TRES_GRANDE';
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. Enrichir les statuts de souscription_organisation
|
||||
-- ============================================================================
|
||||
|
||||
-- Ajout des statuts de cycle de vie avancés
|
||||
ALTER TABLE souscriptions_organisation ADD COLUMN IF NOT EXISTS date_expiration TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE souscriptions_organisation ADD COLUMN IF NOT EXISTS jours_grace INTEGER DEFAULT 7;
|
||||
ALTER TABLE souscriptions_organisation ADD COLUMN IF NOT EXISTS auto_renouvellement BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE souscriptions_organisation ADD COLUMN IF NOT EXISTS motif_suspension TEXT;
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. Index sur plan_commercial pour filtrage catalogue
|
||||
-- ============================================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_formule_plan_commercial ON formules_abonnement (plan_commercial);
|
||||
@@ -0,0 +1,149 @@
|
||||
-- ============================================================================
|
||||
-- V20 — Enrichissement StatutMembre + Seed permissions atomiques
|
||||
-- Les permissions suivent le format : MODULE > RESSOURCE > ACTION
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Statut INVITE sur membre_organisation
|
||||
-- La colonne statut_membre est VARCHAR(30), pas d'enum SQL → simple check de longueur
|
||||
-- Aucune modification DDL nécessaire, le type Java StatutMembre.INVITE est suffisant
|
||||
-- ============================================================================
|
||||
|
||||
-- Vérifier que la colonne statut_membre est assez large pour les nouveaux statuts
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'membre_organisation'
|
||||
AND column_name = 'statut_membre'
|
||||
AND character_maximum_length < 30
|
||||
) THEN
|
||||
ALTER TABLE membre_organisation ALTER COLUMN statut_membre TYPE VARCHAR(30);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Ajouter les colonnes de cycle de vie enrichies
|
||||
ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS date_invitation TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS date_expiration_invitation TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS token_invitation VARCHAR(255);
|
||||
ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS invite_par UUID;
|
||||
ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS motif_archivage TEXT;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Seed permissions atomiques
|
||||
-- Modules : ORGANISATION, MEMBRE, COTISATION, FINANCE, TONTINE, EPARGNE,
|
||||
-- CREDIT, NOTIFICATION, AUDIT, RAPPORT, CONFIGURATION, AGRICULTURE,
|
||||
-- ONG, CULTE, VOTE, COLLECTE, REGISTRE
|
||||
-- Actions : CREATE, READ, UPDATE, DELETE, VALIDATE, EXPORT, IMPORT, SEND
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO permissions (id, code, module, ressource, action, libelle, description, actif, date_creation, date_modification, cree_par, modifie_par, version)
|
||||
SELECT gen_random_uuid(), v.code, v.module, v.ressource, v.action, v.libelle, v.description, true, NOW(), NOW(), 'system', 'system', 0
|
||||
FROM (VALUES
|
||||
|
||||
-- ── ORGANISATION ──────────────────────────────────────────────────────────
|
||||
('ORGANISATION > ORGANISATION > CREATE', 'ORGANISATION', 'ORGANISATION', 'CREATE', 'Créer une organisation', 'Créer une nouvelle organisation sur la plateforme'),
|
||||
('ORGANISATION > ORGANISATION > READ', 'ORGANISATION', 'ORGANISATION', 'READ', 'Consulter une organisation', 'Voir les détails d''une organisation'),
|
||||
('ORGANISATION > ORGANISATION > UPDATE', 'ORGANISATION', 'ORGANISATION', 'UPDATE', 'Modifier une organisation', 'Modifier les informations d''une organisation'),
|
||||
('ORGANISATION > ORGANISATION > DELETE', 'ORGANISATION', 'ORGANISATION', 'DELETE', 'Supprimer une organisation', 'Archiver ou supprimer une organisation'),
|
||||
('ORGANISATION > ORGANISATION > EXPORT', 'ORGANISATION', 'ORGANISATION', 'EXPORT', 'Exporter les données organisation','Exporter les données de l''organisation'),
|
||||
|
||||
-- ── MEMBRE ────────────────────────────────────────────────────────────────
|
||||
('MEMBRE > MEMBRE > CREATE', 'MEMBRE', 'MEMBRE', 'CREATE', 'Créer un membre', 'Ajouter un nouveau membre'),
|
||||
('MEMBRE > MEMBRE > READ', 'MEMBRE', 'MEMBRE', 'READ', 'Consulter un membre', 'Voir la fiche d''un membre'),
|
||||
('MEMBRE > MEMBRE > UPDATE', 'MEMBRE', 'MEMBRE', 'UPDATE', 'Modifier un membre', 'Modifier les informations d''un membre'),
|
||||
('MEMBRE > MEMBRE > DELETE', 'MEMBRE', 'MEMBRE', 'DELETE', 'Désactiver un membre', 'Désactiver ou archiver un membre'),
|
||||
('MEMBRE > MEMBRE > EXPORT', 'MEMBRE', 'MEMBRE', 'EXPORT', 'Exporter les membres', 'Exporter la liste des membres'),
|
||||
('MEMBRE > MEMBRE > IMPORT', 'MEMBRE', 'MEMBRE', 'IMPORT', 'Importer des membres', 'Importer une liste de membres'),
|
||||
('MEMBRE > INVITATION > SEND', 'MEMBRE', 'INVITATION', 'SEND', 'Inviter un membre', 'Envoyer une invitation à rejoindre l''organisation'),
|
||||
('MEMBRE > STATUT > UPDATE', 'MEMBRE', 'STATUT', 'UPDATE', 'Changer le statut d''un membre', 'Activer, suspendre, archiver un membre'),
|
||||
|
||||
-- ── COTISATION ────────────────────────────────────────────────────────────
|
||||
('COTISATION > COTISATION > CREATE', 'COTISATION', 'COTISATION', 'CREATE', 'Créer une cotisation', 'Enregistrer une cotisation'),
|
||||
('COTISATION > COTISATION > READ', 'COTISATION', 'COTISATION', 'READ', 'Consulter les cotisations', 'Voir les cotisations'),
|
||||
('COTISATION > COTISATION > UPDATE', 'COTISATION', 'COTISATION', 'UPDATE', 'Modifier une cotisation', 'Modifier une cotisation'),
|
||||
('COTISATION > COTISATION > DELETE', 'COTISATION', 'COTISATION', 'DELETE', 'Supprimer une cotisation', 'Supprimer une cotisation'),
|
||||
('COTISATION > COTISATION > VALIDATE', 'COTISATION', 'COTISATION', 'VALIDATE', 'Valider une cotisation', 'Valider le paiement d''une cotisation'),
|
||||
('COTISATION > COTISATION > EXPORT', 'COTISATION', 'COTISATION', 'EXPORT', 'Exporter les cotisations', 'Exporter le registre des cotisations'),
|
||||
|
||||
-- ── FINANCE ───────────────────────────────────────────────────────────────
|
||||
('FINANCE > TRANSACTION > CREATE', 'FINANCE', 'TRANSACTION', 'CREATE', 'Créer une transaction', 'Enregistrer une transaction financière'),
|
||||
('FINANCE > TRANSACTION > READ', 'FINANCE', 'TRANSACTION', 'READ', 'Consulter les transactions', 'Voir les transactions financières'),
|
||||
('FINANCE > TRANSACTION > VALIDATE', 'FINANCE', 'TRANSACTION', 'VALIDATE', 'Valider une transaction', 'Approuver une transaction en attente'),
|
||||
('FINANCE > BUDGET > READ', 'FINANCE', 'BUDGET', 'READ', 'Consulter le budget', 'Voir le budget de l''organisation'),
|
||||
('FINANCE > BUDGET > UPDATE', 'FINANCE', 'BUDGET', 'UPDATE', 'Modifier le budget', 'Modifier les lignes budgétaires'),
|
||||
('FINANCE > RAPPORT > READ', 'FINANCE', 'RAPPORT', 'READ', 'Consulter les rapports financiers','Voir les rapports et bilans financiers'),
|
||||
('FINANCE > RAPPORT > EXPORT', 'FINANCE', 'RAPPORT', 'EXPORT', 'Exporter les rapports financiers','Télécharger les rapports financiers'),
|
||||
|
||||
-- ── TONTINE ───────────────────────────────────────────────────────────────
|
||||
('TONTINE > TONTINE > CREATE', 'TONTINE', 'TONTINE', 'CREATE', 'Créer une tontine', 'Créer un nouveau cycle de tontine'),
|
||||
('TONTINE > TONTINE > READ', 'TONTINE', 'TONTINE', 'READ', 'Consulter une tontine', 'Voir les détails d''une tontine'),
|
||||
('TONTINE > TONTINE > UPDATE', 'TONTINE', 'TONTINE', 'UPDATE', 'Modifier une tontine', 'Modifier un cycle de tontine'),
|
||||
('TONTINE > ROTATION > MANAGE', 'TONTINE', 'ROTATION', 'MANAGE', 'Gérer les rotations', 'Gérer l''ordre de rotation des membres'),
|
||||
('TONTINE > COTISATION > CREATE','TONTINE', 'COTISATION', 'CREATE', 'Enregistrer une cotisation tontine', 'Enregistrer le paiement d''une cotisation de tontine'),
|
||||
|
||||
-- ── EPARGNE ───────────────────────────────────────────────────────────────
|
||||
('EPARGNE > COMPTE > CREATE', 'EPARGNE', 'COMPTE', 'CREATE', 'Ouvrir un compte épargne', 'Créer un compte épargne pour un membre'),
|
||||
('EPARGNE > COMPTE > READ', 'EPARGNE', 'COMPTE', 'READ', 'Consulter un compte épargne', 'Voir un compte épargne'),
|
||||
('EPARGNE > TRANSACTION > CREATE', 'EPARGNE', 'TRANSACTION', 'CREATE', 'Effectuer une transaction épargne','Dépôt ou retrait sur compte épargne'),
|
||||
('EPARGNE > TRANSACTION > READ', 'EPARGNE', 'TRANSACTION', 'READ', 'Consulter les transactions épargne','Voir l''historique des transactions'),
|
||||
|
||||
-- ── CREDIT ────────────────────────────────────────────────────────────────
|
||||
('CREDIT > DEMANDE > CREATE', 'CREDIT', 'DEMANDE', 'CREATE', 'Soumettre une demande de crédit', 'Déposer un dossier de crédit'),
|
||||
('CREDIT > DEMANDE > READ', 'CREDIT', 'DEMANDE', 'READ', 'Consulter une demande de crédit', 'Voir un dossier de crédit'),
|
||||
('CREDIT > DEMANDE > VALIDATE', 'CREDIT', 'DEMANDE', 'VALIDATE', 'Approuver/Rejeter un crédit', 'Instruire et décider sur un dossier de crédit'),
|
||||
('CREDIT > DECAISSEMENT > CREATE','CREDIT','DECAISSEMENT','CREATE','Décaisser un crédit', 'Exécuter le décaissement d''un crédit approuvé'),
|
||||
|
||||
-- ── NOTIFICATION ─────────────────────────────────────────────────────────
|
||||
('NOTIFICATION > NOTIFICATION > CREATE', 'NOTIFICATION', 'NOTIFICATION', 'CREATE', 'Créer une notification', 'Envoyer une notification'),
|
||||
('NOTIFICATION > NOTIFICATION > READ', 'NOTIFICATION', 'NOTIFICATION', 'READ', 'Lire ses notifications', 'Voir ses propres notifications'),
|
||||
('NOTIFICATION > TEMPLATE > CREATE', 'NOTIFICATION', 'TEMPLATE', 'CREATE', 'Créer un template', 'Créer un template de notification'),
|
||||
('NOTIFICATION > GROUPE > SEND', 'NOTIFICATION', 'GROUPE', 'SEND', 'Envoyer en masse', 'Envoyer des notifications groupées'),
|
||||
|
||||
-- ── AUDIT ─────────────────────────────────────────────────────────────────
|
||||
('AUDIT > LOG > READ', 'AUDIT', 'LOG', 'READ', 'Consulter les logs d''audit', 'Voir les logs et traces d''activité'),
|
||||
('AUDIT > LOG > EXPORT', 'AUDIT', 'LOG', 'EXPORT', 'Exporter les logs', 'Télécharger les logs d''audit'),
|
||||
('AUDIT > STATISTIQUES > READ', 'AUDIT', 'STATISTIQUES', 'READ', 'Voir les statistiques d''audit', 'Analyser les métriques d''activité'),
|
||||
|
||||
-- ── RAPPORT ───────────────────────────────────────────────────────────────
|
||||
('RAPPORT > RAPPORT > READ', 'RAPPORT', 'RAPPORT', 'READ', 'Consulter un rapport', 'Voir les rapports de l''organisation'),
|
||||
('RAPPORT > RAPPORT > EXPORT', 'RAPPORT', 'RAPPORT', 'EXPORT', 'Exporter un rapport', 'Télécharger un rapport PDF/Excel'),
|
||||
('RAPPORT > RAPPORT > CREATE', 'RAPPORT', 'RAPPORT', 'CREATE', 'Générer un rapport', 'Générer un nouveau rapport'),
|
||||
|
||||
-- ── CONFIGURATION ─────────────────────────────────────────────────────────
|
||||
('CONFIGURATION > ORGANISATION > UPDATE', 'CONFIGURATION', 'ORGANISATION', 'UPDATE', 'Configurer l''organisation', 'Modifier les paramètres de l''organisation'),
|
||||
('CONFIGURATION > ROLE > MANAGE', 'CONFIGURATION', 'ROLE', 'MANAGE', 'Gérer les rôles', 'Assigner ou révoquer des rôles aux membres'),
|
||||
('CONFIGURATION > MODULE > MANAGE', 'CONFIGURATION', 'MODULE', 'MANAGE', 'Gérer les modules actifs', 'Activer ou désactiver des modules'),
|
||||
|
||||
-- ── AGRICULTURE / COOPERATIVE ─────────────────────────────────────────────
|
||||
('AGRICULTURE > CAMPAGNE > CREATE', 'AGRICULTURE', 'CAMPAGNE', 'CREATE', 'Créer une campagne agricole', 'Créer et planifier une campagne'),
|
||||
('AGRICULTURE > CAMPAGNE > READ', 'AGRICULTURE', 'CAMPAGNE', 'READ', 'Consulter une campagne agricole', 'Voir les détails d''une campagne'),
|
||||
('AGRICULTURE > CAMPAGNE > UPDATE', 'AGRICULTURE', 'CAMPAGNE', 'UPDATE', 'Modifier une campagne agricole', 'Modifier une campagne en cours'),
|
||||
|
||||
-- ── ONG / PROJETS ─────────────────────────────────────────────────────────
|
||||
('ONG > PROJET > CREATE', 'ONG', 'PROJET', 'CREATE', 'Créer un projet ONG', 'Créer et planifier un projet'),
|
||||
('ONG > PROJET > READ', 'ONG', 'PROJET', 'READ', 'Consulter un projet ONG', 'Voir les détails d''un projet'),
|
||||
('ONG > PROJET > UPDATE', 'ONG', 'PROJET', 'UPDATE', 'Modifier un projet ONG', 'Modifier un projet en cours'),
|
||||
('ONG > COLLECTE > CREATE', 'ONG', 'COLLECTE', 'CREATE', 'Lancer une collecte de fonds', 'Créer une campagne de collecte'),
|
||||
|
||||
-- ── CULTE / DONS ──────────────────────────────────────────────────────────
|
||||
('CULTE > DON > CREATE', 'CULTE', 'DON', 'CREATE', 'Enregistrer un don religieux', 'Enregistrer un don cultuel'),
|
||||
('CULTE > DON > READ', 'CULTE', 'DON', 'READ', 'Consulter les dons religieux', 'Voir les dons et contributions cultuels'),
|
||||
|
||||
-- ── VOTE ─────────────────────────────────────────────────────────────────
|
||||
('VOTE > CAMPAGNE > CREATE', 'VOTE', 'CAMPAGNE', 'CREATE', 'Créer une campagne de vote', 'Lancer une élection ou un référendum'),
|
||||
('VOTE > CAMPAGNE > READ', 'VOTE', 'CAMPAGNE', 'READ', 'Consulter une campagne vote', 'Voir une campagne de vote'),
|
||||
('VOTE > BULLETIN > CREATE', 'VOTE', 'BULLETIN', 'CREATE', 'Voter', 'Déposer son bulletin de vote'),
|
||||
|
||||
-- ── REGISTRE / AGREMENT ───────────────────────────────────────────────────
|
||||
('REGISTRE > AGREMENT > CREATE', 'REGISTRE', 'AGREMENT', 'CREATE', 'Enregistrer un agrément', 'Créer une accréditation professionnelle'),
|
||||
('REGISTRE > AGREMENT > READ', 'REGISTRE', 'AGREMENT', 'READ', 'Consulter les agréments', 'Voir les agréments et accréditations')
|
||||
|
||||
) AS v(code, module, ressource, action, libelle, description)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE permissions.code = v.code);
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. Index sur les colonnes d'invitation
|
||||
-- ============================================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_mo_token_invitation ON membre_organisation (token_invitation);
|
||||
CREATE INDEX IF NOT EXISTS idx_mo_date_invitation ON membre_organisation (date_invitation);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- V21 : Colonnes cycle de vie membres (MemberLifecycleService)
|
||||
-- Ajout de role_org sur membre_organisation (manquant de V20)
|
||||
-- + index sur token_invitation (version courte 32 chars → VARCHAR(64))
|
||||
|
||||
-- Ajouter role_org si absent
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'membre_organisation' AND column_name = 'role_org'
|
||||
) THEN
|
||||
ALTER TABLE membre_organisation ADD COLUMN role_org VARCHAR(50);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- Ajuster longueur token_invitation à 64 pour UUID sans tirets (32 chars) ou extensions futures
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'membre_organisation' AND column_name = 'token_invitation'
|
||||
AND character_maximum_length = 255
|
||||
) THEN
|
||||
ALTER TABLE membre_organisation ALTER COLUMN token_invitation TYPE VARCHAR(64);
|
||||
END IF;
|
||||
END$$;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- V22 : Barème de cotisation par rôle + flag génération automatique mensuelle
|
||||
-- Auteur : UnionFlow Team
|
||||
-- Date : 2026-04-07
|
||||
|
||||
-- 1. Activation génération automatique dans les paramètres de cotisation
|
||||
ALTER TABLE parametres_cotisation_organisation
|
||||
ADD COLUMN IF NOT EXISTS generation_automatique_activee BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- 2. Table des barèmes de cotisation par rôle fonctionnel
|
||||
CREATE TABLE IF NOT EXISTS bareme_cotisation_role (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
organisation_id UUID NOT NULL,
|
||||
role_org VARCHAR(50) NOT NULL,
|
||||
montant_mensuel NUMERIC(12,2) NOT NULL DEFAULT 0.00,
|
||||
montant_annuel NUMERIC(12,2) NOT NULL DEFAULT 0.00,
|
||||
description VARCHAR(255),
|
||||
-- BaseEntity columns
|
||||
date_creation TIMESTAMP,
|
||||
date_modification TIMESTAMP,
|
||||
cree_par VARCHAR(255),
|
||||
modifie_par VARCHAR(255),
|
||||
version BIGINT DEFAULT 0,
|
||||
actif BOOLEAN DEFAULT TRUE,
|
||||
CONSTRAINT pk_bareme_cotisation_role PRIMARY KEY (id),
|
||||
CONSTRAINT fk_bareme_cot_organisation
|
||||
FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT uq_bareme_cot_org_role
|
||||
UNIQUE (organisation_id, role_org)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bareme_cot_org ON bareme_cotisation_role (organisation_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- V23: Fix system_alerts legacy NOT NULL columns
|
||||
--
|
||||
-- Contexte: L'entité SystemAlert a été refactorisée pour utiliser les colonnes
|
||||
-- modernes (alert_type, level, title) ajoutées par hibernate.ddl-auto=update.
|
||||
-- Les anciennes colonnes V1 (type_alerte, severite, titre) restaient NOT NULL
|
||||
-- et provoquaient un ConstraintViolationException toutes les 60 secondes dans
|
||||
-- AlertMonitoringService#monitorSystemMetrics.
|
||||
--
|
||||
-- Fix: rendre ces colonnes obsolètes nullable pour que l'INSERT Hibernate réussisse.
|
||||
|
||||
ALTER TABLE system_alerts ALTER COLUMN type_alerte DROP NOT NULL;
|
||||
ALTER TABLE system_alerts ALTER COLUMN severite DROP NOT NULL;
|
||||
ALTER TABLE system_alerts ALTER COLUMN titre DROP NOT NULL;
|
||||
Reference in New Issue
Block a user