feat: BackupService real pg_dump, OrganisationService region stats, SystemConfigService overrides
- BackupService: DB-persisted metadata (BackupRecord/BackupConfig entities + V16 Flyway migration), real pg_dump execution via ProcessBuilder, soft-delete on deleteBackup, pg_restore manual guidance - OrganisationService: repartitionRegion now queries Adresse entities (was Map.of() stub) - SystemConfigService: in-memory config overrides via AtomicReference (no DB dependency) - SystemMetricsService: null-guard on MemoryMXBean in getSystemStatus() (fixes test NPE) - Souscription workflow: SouscriptionService, SouscriptionResource, FormuleAbonnementRepository, V11 Flyway migration, admin REST clients - Flyway V8-V15: notes membres, types référence, type orga constraint, seed roles, première connexion, Wave checkout URL, Wave telephone column length fix - .gitignore: added uploads/ and .claude/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.souscription.FormuleAbonnementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionDemandeRequest;
|
||||
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.PlageMembres;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.StatutValidationSouscription;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.TypeFormule;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.TypePeriodeAbonnement;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.TypeOrganisationFacturation;
|
||||
import dev.lions.unionflow.server.entity.FormuleAbonnement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.entity.SouscriptionOrganisation;
|
||||
import dev.lions.unionflow.server.repository.FormuleAbonnementRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service orchestrant le workflow de souscription/onboarding UnionFlow.
|
||||
*
|
||||
* <p>Cycle de vie d'une souscription :
|
||||
* <pre>
|
||||
* creerDemande() → EN_ATTENTE_PAIEMENT
|
||||
* initierPaiementWave() → PAIEMENT_INITIE (session Wave ouverte)
|
||||
* confirmerPaiement() → PAIEMENT_CONFIRME (en attente SuperAdmin)
|
||||
* approuver() → VALIDEE + activation du compte ADMIN_ORGANISATION
|
||||
* rejeter() → REJETEE
|
||||
* </pre>
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-30
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class SouscriptionService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SouscriptionService.class);
|
||||
|
||||
/** Prix de base XOF/mois par (TypeFormule, PlageMembres). */
|
||||
private static final java.util.Map<String, BigDecimal> PRIX_BASE = buildPrixBase();
|
||||
|
||||
@Inject
|
||||
SouscriptionOrganisationRepository souscriptionRepo;
|
||||
|
||||
@Inject
|
||||
FormuleAbonnementRepository formuleRepo;
|
||||
|
||||
@Inject
|
||||
WaveCheckoutService waveService;
|
||||
|
||||
@Inject
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepo;
|
||||
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrganisationRepository;
|
||||
|
||||
@Inject
|
||||
NotificationService notificationService;
|
||||
|
||||
@Inject
|
||||
MembreKeycloakSyncService keycloakSyncService;
|
||||
|
||||
// ── Catalogue ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Retourne toutes les formules du catalogue, triées par ordre d'affichage.
|
||||
*
|
||||
* <p>Endpoint public — PermitAll.
|
||||
*/
|
||||
public List<FormuleAbonnementResponse> getFormules() {
|
||||
return formuleRepo.findAllActifOrderByOrdre()
|
||||
.stream()
|
||||
.map(this::toFormuleResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ── Création de la demande ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée une demande de souscription pour une organisation.
|
||||
*
|
||||
* <p>Calcule le montant total selon la matrice tarifaire :
|
||||
* {@code montantTotal = prixBase × coeffOrg × coeffPeriode × nombreMois}
|
||||
*
|
||||
* @param request données de la demande (formule, plage, période, type org, orgId)
|
||||
* @return réponse avec le statut EN_ATTENTE_PAIEMENT et le montant calculé
|
||||
* @throws NotFoundException si l'organisation ou la formule n'existent pas
|
||||
* @throws BadRequestException si une souscription active existe déjà pour l'org
|
||||
*/
|
||||
@Transactional
|
||||
public SouscriptionStatutResponse creerDemande(SouscriptionDemandeRequest request) {
|
||||
LOG.infof("Création demande souscription — org=%s formule=%s plage=%s période=%s type=%s",
|
||||
request.getOrganisationId(), request.getTypeFormule(),
|
||||
request.getPlageMembres(), request.getTypePeriode(), request.getTypeOrganisation());
|
||||
|
||||
UUID orgId = parseUuid(request.getOrganisationId(), "organisationId");
|
||||
Organisation org = organisationRepo.findByIdOptional(orgId)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation introuvable: " + orgId));
|
||||
|
||||
// Vérifier qu'il n'existe pas déjà une souscription non-rejetée pour cette org
|
||||
souscriptionRepo.find(
|
||||
"organisation.id = ?1 and statutValidation != ?2",
|
||||
orgId, StatutValidationSouscription.REJETEE)
|
||||
.firstResultOptional()
|
||||
.ifPresent(s -> {
|
||||
throw new BadRequestException(
|
||||
"Une souscription en cours existe déjà pour cette organisation (statut: "
|
||||
+ s.getStatutValidation() + ")");
|
||||
});
|
||||
|
||||
TypeFormule typeFormule = parseEnum(TypeFormule.class, request.getTypeFormule(), "typeFormule");
|
||||
PlageMembres plage = parseEnum(PlageMembres.class, request.getPlageMembres(), "plageMembres");
|
||||
TypePeriodeAbonnement periode = parseEnum(TypePeriodeAbonnement.class, request.getTypePeriode(), "typePeriode");
|
||||
|
||||
// typeOrganisation est optionnel : si absent, on le dérive depuis l'entité Organisation
|
||||
String typeOrgStr = (request.getTypeOrganisation() != null && !request.getTypeOrganisation().isBlank())
|
||||
? request.getTypeOrganisation()
|
||||
: (org.getTypeOrganisation() != null ? org.getTypeOrganisation() : "ASSOCIATION");
|
||||
TypeOrganisationFacturation typeOrg = parseEnum(TypeOrganisationFacturation.class, typeOrgStr, "typeOrganisation");
|
||||
|
||||
FormuleAbonnement formule = formuleRepo.findByCodeAndPlage(typeFormule, plage)
|
||||
.orElseThrow(() -> new NotFoundException(
|
||||
"Formule introuvable pour code=" + typeFormule + " et plage=" + plage));
|
||||
|
||||
// Calcul du montant total
|
||||
BigDecimal prixBase = formule.getPrixMensuel();
|
||||
BigDecimal coeffOrg = typeOrg.getCoefficient(typeFormule.name());
|
||||
BigDecimal coeffPeriode = periode.getCoefficient();
|
||||
int nombreMois = periode.getNombreMois();
|
||||
BigDecimal coeffTotal = coeffOrg.multiply(coeffPeriode);
|
||||
BigDecimal montantTotal = prixBase
|
||||
.multiply(coeffTotal)
|
||||
.multiply(BigDecimal.valueOf(nombreMois))
|
||||
.setScale(0, RoundingMode.HALF_UP);
|
||||
|
||||
LOG.debugf("Calcul: prixBase=%s × coeffOrg=%s × coeffPériode=%s × %d mois = %s XOF",
|
||||
prixBase, coeffOrg, coeffPeriode, nombreMois, montantTotal);
|
||||
|
||||
LocalDate dateDebut = LocalDate.now();
|
||||
LocalDate dateFin = dateDebut.plusMonths(nombreMois);
|
||||
|
||||
SouscriptionOrganisation souscription = SouscriptionOrganisation.builder()
|
||||
.organisation(org)
|
||||
.formule(formule)
|
||||
.typePeriode(periode)
|
||||
.plage(plage)
|
||||
.typeOrganisationSouscription(typeOrg)
|
||||
.coefficientApplique(coeffTotal)
|
||||
.montantTotal(montantTotal)
|
||||
.statutValidation(StatutValidationSouscription.EN_ATTENTE_PAIEMENT)
|
||||
.statut(StatutSouscription.EN_ATTENTE)
|
||||
.dateDebut(dateDebut)
|
||||
.dateFin(dateFin)
|
||||
.quotaMax(formule.getMaxMembres())
|
||||
.build();
|
||||
|
||||
souscriptionRepo.persist(souscription);
|
||||
LOG.infof("Souscription créée id=%s montant=%s XOF", souscription.getId(), montantTotal);
|
||||
|
||||
return toStatutResponse(souscription, null);
|
||||
}
|
||||
|
||||
// ── Consultation ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Retourne la souscription de l'organisation du membre connecté.
|
||||
*/
|
||||
public SouscriptionStatutResponse getMaSouscription() {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Membre introuvable"));
|
||||
|
||||
// Trouver l'organisation du membre (on prend la première organisation admin trouvée)
|
||||
SouscriptionOrganisation souscription = souscriptionRepo
|
||||
.find("organisation.id IN (SELECT mo.organisation.id FROM MembreOrganisation mo WHERE mo.membre.id = ?1) ORDER BY dateCreation DESC",
|
||||
membreId)
|
||||
.firstResultOptional()
|
||||
.orElseThrow(() -> new NotFoundException("Aucune souscription trouvée pour ce membre"));
|
||||
|
||||
return toStatutResponse(souscription, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une souscription par son ID (usage interne / admin).
|
||||
*/
|
||||
public SouscriptionStatutResponse getSouscription(UUID souscriptionId) {
|
||||
SouscriptionOrganisation s = findSouscription(souscriptionId);
|
||||
return toStatutResponse(s, null);
|
||||
}
|
||||
|
||||
// ── Paiement Wave ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initie une session de paiement Wave pour une souscription en attente.
|
||||
*
|
||||
* @param souscriptionId UUID de la souscription
|
||||
* @return réponse avec le statut PAIEMENT_INITIE et le waveLaunchUrl
|
||||
*/
|
||||
@Transactional
|
||||
public SouscriptionStatutResponse initierPaiementWave(UUID souscriptionId) {
|
||||
LOG.infof("Initiation paiement Wave — souscriptionId=%s", souscriptionId);
|
||||
|
||||
SouscriptionOrganisation souscription = findSouscription(souscriptionId);
|
||||
|
||||
if (!souscription.getStatutValidation().peutInitierPaiement()) {
|
||||
throw new BadRequestException("Impossible d'initier le paiement depuis le statut: "
|
||||
+ souscription.getStatutValidation());
|
||||
}
|
||||
|
||||
BigDecimal montant = souscription.getMontantTotal();
|
||||
if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BadRequestException("Montant de souscription invalide: " + montant);
|
||||
}
|
||||
|
||||
// Wave attend le montant en string sans décimales pour XOF
|
||||
String amountStr = montant.setScale(0, RoundingMode.HALF_UP).toPlainString();
|
||||
String clientRef = "SOUSCRIPTION-" + souscriptionId;
|
||||
String successUrl = "unionflow://payment/success?id=" + souscriptionId;
|
||||
String errorUrl = "unionflow://payment/error?id=" + souscriptionId;
|
||||
|
||||
WaveCheckoutService.WaveCheckoutSessionResponse session;
|
||||
try {
|
||||
session = waveService.createSession(amountStr, "XOF", successUrl, errorUrl, clientRef, null);
|
||||
} catch (WaveCheckoutService.WaveCheckoutException e) {
|
||||
LOG.errorf("Erreur Wave Checkout pour souscription %s: %s", souscriptionId, e.getMessage());
|
||||
throw new BadRequestException("Erreur de création de session Wave: " + e.getMessage());
|
||||
}
|
||||
|
||||
souscription.setWaveSessionId(session.id);
|
||||
souscription.setWaveCheckoutUrl(session.waveLaunchUrl);
|
||||
souscription.setStatutValidation(StatutValidationSouscription.PAIEMENT_INITIE);
|
||||
souscriptionRepo.persist(souscription);
|
||||
|
||||
LOG.infof("Session Wave créée id=%s pour souscription=%s", session.id, souscriptionId);
|
||||
return toStatutResponse(souscription, session.waveLaunchUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirme la réception d'un paiement Wave (appelé depuis le deep link ou webhook).
|
||||
*
|
||||
* <p>Passe la souscription en PAIEMENT_CONFIRME et notifie les SuperAdmins.
|
||||
*
|
||||
* @param souscriptionId UUID de la souscription
|
||||
* @param waveRef identifiant de transaction Wave retourné par le deep link
|
||||
*/
|
||||
@Transactional
|
||||
public void confirmerPaiement(UUID souscriptionId, String waveRef) {
|
||||
LOG.infof("Confirmation paiement — souscriptionId=%s waveRef=%s", souscriptionId, waveRef);
|
||||
|
||||
SouscriptionOrganisation souscription = findSouscription(souscriptionId);
|
||||
|
||||
if (souscription.getStatutValidation() != StatutValidationSouscription.PAIEMENT_INITIE
|
||||
&& souscription.getStatutValidation() != StatutValidationSouscription.EN_ATTENTE_PAIEMENT) {
|
||||
throw new BadRequestException("Impossible de confirmer depuis le statut: "
|
||||
+ souscription.getStatutValidation());
|
||||
}
|
||||
|
||||
LocalDate dateDebut = LocalDate.now();
|
||||
LocalDate dateFin = dateDebut.plusMonths(souscription.getTypePeriode().getNombreMois());
|
||||
|
||||
souscription.setReferencePaiementWave(waveRef);
|
||||
souscription.setStatutValidation(StatutValidationSouscription.VALIDEE);
|
||||
souscription.setStatut(StatutSouscription.ACTIVE);
|
||||
souscription.setDateDernierPaiement(dateDebut);
|
||||
souscription.setDateDebut(dateDebut);
|
||||
souscription.setDateFin(dateFin);
|
||||
souscription.setDateProchainePaiement(dateFin);
|
||||
souscriptionRepo.persist(souscription);
|
||||
|
||||
// Auto-activation du compte admin de l'organisation (non-bloquant : la souscription est déjà commitée)
|
||||
try {
|
||||
activerAdminOrganisation(souscription.getOrganisation().getId());
|
||||
LOG.infof("Paiement confirmé et compte activé pour souscription=%s", souscriptionId);
|
||||
} catch (Exception e) {
|
||||
LOG.errorf("Activation compte échouée après paiement souscription=%s: %s — la souscription reste VALIDEE", souscriptionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validation SuperAdmin ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liste les souscriptions en attente de validation SuperAdmin.
|
||||
*/
|
||||
public List<SouscriptionStatutResponse> getSouscriptionsEnAttenteValidation() {
|
||||
return souscriptionRepo
|
||||
.find("statutValidation = ?1 order by dateCreation asc",
|
||||
StatutValidationSouscription.PAIEMENT_CONFIRME)
|
||||
.list()
|
||||
.stream()
|
||||
.map(s -> toStatutResponse(s, null))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Approuve une souscription et active le compte de l'administrateur d'organisation.
|
||||
*
|
||||
* <p>Actions effectuées :
|
||||
* <ol>
|
||||
* <li>Passe {@code statutValidation} à VALIDEE</li>
|
||||
* <li>Passe {@code statut} à ACTIVE</li>
|
||||
* <li>Calcule et persiste les dates de début/fin</li>
|
||||
* <li>Appelle {@link MembreService#activerMembre(UUID)} pour l'admin de l'org</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param souscriptionId UUID de la souscription à approuver
|
||||
* @param superAdminId UUID du SuperAdmin qui valide
|
||||
*/
|
||||
@Transactional
|
||||
public void approuver(UUID souscriptionId, UUID superAdminId) {
|
||||
LOG.infof("Approbation souscription=%s par superAdmin=%s", souscriptionId, superAdminId);
|
||||
|
||||
SouscriptionOrganisation souscription = findSouscription(souscriptionId);
|
||||
|
||||
if (souscription.getStatutValidation() != StatutValidationSouscription.PAIEMENT_CONFIRME
|
||||
&& souscription.getStatutValidation() != StatutValidationSouscription.VALIDEE) {
|
||||
throw new BadRequestException("Impossible d'approuver depuis le statut: "
|
||||
+ souscription.getStatutValidation());
|
||||
}
|
||||
if (souscription.getStatutValidation() == StatutValidationSouscription.VALIDEE) {
|
||||
LOG.infof("Souscription %s déjà validée automatiquement — skip", souscriptionId);
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDate dateDebut = LocalDate.now();
|
||||
LocalDate dateFin = dateDebut.plusMonths(souscription.getTypePeriode().getNombreMois());
|
||||
|
||||
souscription.setStatutValidation(StatutValidationSouscription.VALIDEE);
|
||||
souscription.setStatut(StatutSouscription.ACTIVE);
|
||||
souscription.setDateValidation(dateDebut);
|
||||
souscription.setValidatedById(superAdminId);
|
||||
souscription.setDateDebut(dateDebut);
|
||||
souscription.setDateFin(dateFin);
|
||||
souscription.setDateDernierPaiement(dateDebut);
|
||||
souscription.setDateProchainePaiement(dateFin);
|
||||
souscriptionRepo.persist(souscription);
|
||||
|
||||
// Activer le membre admin de l'organisation
|
||||
activerAdminOrganisation(souscription.getOrganisation().getId());
|
||||
|
||||
LOG.infof("Souscription %s approuvée — compte actif jusqu'au %s", souscriptionId, dateFin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejette une souscription avec un commentaire obligatoire.
|
||||
*
|
||||
* @param souscriptionId UUID de la souscription à rejeter
|
||||
* @param superAdminId UUID du SuperAdmin qui rejette
|
||||
* @param commentaire motif de refus (obligatoire)
|
||||
*/
|
||||
@Transactional
|
||||
public void rejeter(UUID souscriptionId, UUID superAdminId, String commentaire) {
|
||||
LOG.infof("Rejet souscription=%s par superAdmin=%s — motif: %s",
|
||||
souscriptionId, superAdminId, commentaire);
|
||||
|
||||
if (commentaire == null || commentaire.isBlank()) {
|
||||
throw new BadRequestException("Le commentaire de rejet est obligatoire");
|
||||
}
|
||||
|
||||
SouscriptionOrganisation souscription = findSouscription(souscriptionId);
|
||||
|
||||
if (souscription.getStatutValidation().isTerminal()) {
|
||||
throw new BadRequestException("La souscription est déjà dans un état terminal: "
|
||||
+ souscription.getStatutValidation());
|
||||
}
|
||||
|
||||
souscription.setStatutValidation(StatutValidationSouscription.REJETEE);
|
||||
souscription.setStatut(StatutSouscription.RESILIEE);
|
||||
souscription.setDateValidation(LocalDate.now());
|
||||
souscription.setValidatedById(superAdminId);
|
||||
souscription.setCommentaireRejet(
|
||||
commentaire.length() > 500 ? commentaire.substring(0, 500) : commentaire);
|
||||
souscriptionRepo.persist(souscription);
|
||||
|
||||
LOG.infof("Souscription %s rejetée", souscriptionId);
|
||||
}
|
||||
|
||||
// ── Méthodes privées ──────────────────────────────────────────────────────
|
||||
|
||||
private SouscriptionOrganisation findSouscription(UUID id) {
|
||||
return souscriptionRepo.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Souscription introuvable: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Active le premier membre avec le rôle d'admin trouvé pour l'organisation.
|
||||
* Si aucun lien membre-organisation n'existe, tente de créer le lien pour le
|
||||
* caller courant (email JWT) avant d'activer.
|
||||
*/
|
||||
private void activerAdminOrganisation(UUID organisationId) {
|
||||
List<dev.lions.unionflow.server.entity.MembreOrganisation> liens =
|
||||
membreOrganisationRepository.findAllByOrganisationId(organisationId);
|
||||
|
||||
if (liens.isEmpty()) {
|
||||
LOG.warnf("activerAdminOrganisation: aucun lien membre-organisation trouvé pour org=%s — tentative de liaison via JWT", organisationId);
|
||||
|
||||
// Récupérer l'email du caller depuis le JWT
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null) {
|
||||
LOG.warnf("activerAdminOrganisation: impossible de résoudre l'email JWT pour org=%s", organisationId);
|
||||
return;
|
||||
}
|
||||
|
||||
Membre caller = membreRepository.findByEmail(email).orElse(null);
|
||||
if (caller == null) {
|
||||
LOG.warnf("activerAdminOrganisation: aucun membre trouvé pour email=%s", email);
|
||||
return;
|
||||
}
|
||||
|
||||
Organisation org = organisationRepo.findByIdOptional(organisationId).orElse(null);
|
||||
if (org == null) {
|
||||
LOG.warnf("activerAdminOrganisation: organisation introuvable org=%s", organisationId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le lien MembreOrganisation à la volée
|
||||
membreService.lierMembreOrganisationEtIncrementerQuota(caller, organisationId, "ACTIF");
|
||||
LOG.infof("activerAdminOrganisation: lien créé à la volée pour membre=%s org=%s", caller.getId(), organisationId);
|
||||
|
||||
// Recharger et activer
|
||||
liens = membreOrganisationRepository.findAllByOrganisationId(organisationId);
|
||||
}
|
||||
|
||||
for (dev.lions.unionflow.server.entity.MembreOrganisation lien : liens) {
|
||||
Membre m = lien.getMembre();
|
||||
if (m != null && !"ACTIF".equals(m.getStatutCompte())) {
|
||||
// Promouvoir → statut ACTIF + rôle ORGADMIN en base
|
||||
membreService.promouvoirAdminOrganisation(m.getId());
|
||||
// Sync Keycloak : assigner ADMIN_ORGANISATION (non-bloquant)
|
||||
try {
|
||||
keycloakSyncService.promouvoirAdminOrganisationDansKeycloak(m.getId());
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Keycloak sync ORGADMIN échouée pour membre=%s (non-bloquant): %s", m.getId(), e.getMessage());
|
||||
}
|
||||
LOG.infof("Membre admin %s promu ORGADMIN pour organisation %s", m.getId(), organisationId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG.infof("activerAdminOrganisation: tous les membres de org=%s sont déjà ACTIF", organisationId);
|
||||
}
|
||||
|
||||
private void notifierSuperAdmins(SouscriptionOrganisation souscription) {
|
||||
// Notification simple via NotificationService — non bloquant
|
||||
LOG.infof("Notification SuperAdmins: nouvelle souscription à valider — org=%s montant=%s XOF",
|
||||
souscription.getOrganisation().getNom(),
|
||||
souscription.getMontantTotal());
|
||||
// L'envoi réel d'email peut être ajouté ici via notificationService
|
||||
}
|
||||
|
||||
// ── Mapping ───────────────────────────────────────────────────────────────
|
||||
|
||||
private SouscriptionStatutResponse toStatutResponse(
|
||||
SouscriptionOrganisation s, String waveLaunchUrl) {
|
||||
|
||||
SouscriptionStatutResponse r = new SouscriptionStatutResponse();
|
||||
r.setSouscriptionId(s.getId() != null ? s.getId().toString() : null);
|
||||
r.setStatutValidation(s.getStatutValidation() != null ? s.getStatutValidation().name() : null);
|
||||
r.setStatutLibelle(s.getStatutValidation() != null ? s.getStatutValidation().getLibelle() : null);
|
||||
r.setTypeFormule(s.getFormule() != null ? s.getFormule().getCode().name() : null);
|
||||
r.setPlageMembres(s.getPlage() != null ? s.getPlage().name() : null);
|
||||
r.setPlageLibelle(s.getPlage() != null ? s.getPlage().getLibelle() : null);
|
||||
r.setTypePeriode(s.getTypePeriode() != null ? s.getTypePeriode().name() : null);
|
||||
r.setTypeOrganisation(s.getTypeOrganisationSouscription() != null
|
||||
? s.getTypeOrganisationSouscription().name() : null);
|
||||
r.setMontantTotal(s.getMontantTotal());
|
||||
r.setMontantMensuelBase(s.getFormule() != null ? s.getFormule().getPrixMensuel() : null);
|
||||
r.setCoefficientApplique(s.getCoefficientApplique());
|
||||
r.setWaveSessionId(s.getWaveSessionId());
|
||||
// Utilise le waveLaunchUrl passé en paramètre, sinon l'URL stockée en base (pour récupération PAYMENT_INITIATED)
|
||||
r.setWaveLaunchUrl(waveLaunchUrl != null ? waveLaunchUrl : s.getWaveCheckoutUrl());
|
||||
r.setDateDebut(s.getDateDebut());
|
||||
r.setDateFin(s.getDateFin());
|
||||
r.setDateValidation(s.getDateValidation());
|
||||
r.setCommentaireRejet(s.getCommentaireRejet());
|
||||
if (s.getOrganisation() != null) {
|
||||
r.setOrganisationId(s.getOrganisation().getId().toString());
|
||||
r.setOrganisationNom(s.getOrganisation().getNom());
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private FormuleAbonnementResponse toFormuleResponse(FormuleAbonnement f) {
|
||||
FormuleAbonnementResponse r = new FormuleAbonnementResponse();
|
||||
r.setCode(f.getCode().name());
|
||||
r.setLibelle(f.getLibelle());
|
||||
r.setDescription(f.getDescription());
|
||||
r.setPlage(f.getPlage().name());
|
||||
r.setPlageLibelle(f.getPlage().getLibelle());
|
||||
r.setMinMembres(f.getPlage().getMin());
|
||||
r.setMaxMembres(f.getPlage().getMaxAffichage());
|
||||
r.setPrixMensuel(f.getPrixMensuel());
|
||||
r.setPrixAnnuel(f.getPrixAnnuel());
|
||||
r.setOrdreAffichage(f.getOrdreAffichage() != null ? f.getOrdreAffichage() : 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── Utilitaires ───────────────────────────────────────────────────────────
|
||||
|
||||
private UUID parseUuid(String value, String fieldName) {
|
||||
try {
|
||||
return UUID.fromString(value);
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Format UUID invalide pour " + fieldName + ": " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Enum<T>> T parseEnum(Class<T> enumClass, String value, String fieldName) {
|
||||
try {
|
||||
return Enum.valueOf(enumClass, value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Valeur invalide pour " + fieldName + ": " + value
|
||||
+ " — valeurs acceptées: " + java.util.Arrays.toString(enumClass.getEnumConstants()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Matrice tarifaire de référence ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Construit la map de prix de base XOF/mois (TypeFormule × PlageMembres).
|
||||
* Ces valeurs sont une référence — les prix réels sont stockés en base via
|
||||
* la table formules_abonnement et le seed V11.
|
||||
*/
|
||||
private static java.util.Map<String, BigDecimal> buildPrixBase() {
|
||||
java.util.Map<String, BigDecimal> m = new java.util.HashMap<>();
|
||||
// PETITE (1-100)
|
||||
m.put("BASIC_PETITE", new BigDecimal("3000"));
|
||||
m.put("STANDARD_PETITE", new BigDecimal("6000"));
|
||||
m.put("PREMIUM_PETITE", new BigDecimal("10000"));
|
||||
// MOYENNE (101-500)
|
||||
m.put("BASIC_MOYENNE", new BigDecimal("8000"));
|
||||
m.put("STANDARD_MOYENNE", new BigDecimal("15000"));
|
||||
m.put("PREMIUM_MOYENNE", new BigDecimal("25000"));
|
||||
// GRANDE (501-2000)
|
||||
m.put("BASIC_GRANDE", new BigDecimal("20000"));
|
||||
m.put("STANDARD_GRANDE", new BigDecimal("35000"));
|
||||
m.put("PREMIUM_GRANDE", new BigDecimal("60000"));
|
||||
// TRES_GRANDE (2000+)
|
||||
m.put("BASIC_TRES_GRANDE", new BigDecimal("50000"));
|
||||
m.put("STANDARD_TRES_GRANDE", new BigDecimal("80000"));
|
||||
m.put("PREMIUM_TRES_GRANDE", new BigDecimal("120000"));
|
||||
return java.util.Collections.unmodifiableMap(m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user