646 lines
31 KiB
Java
646 lines
31 KiB
Java
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 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.
|
||
*/
|
||
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.
|
||
*/
|
||
/**
|
||
* Active l'admin de l'organisation après paiement de la souscription.
|
||
*
|
||
* <p>Stratégie : identifier le membre qui a payé (via JWT) et le promouvoir
|
||
* ORGADMIN. Si le membre n'a pas de lien MembreOrganisation, le créer.
|
||
*
|
||
* <p>Cascades : promouvoirAdminOrganisation (DB) + sync Keycloak ADMIN_ORGANISATION.
|
||
*/
|
||
private void activerAdminOrganisation(UUID organisationId) {
|
||
// ── 1. Identifier le caller (le membre qui a payé) ─────────────────────
|
||
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;
|
||
}
|
||
|
||
LOG.infof("activerAdminOrganisation: caller=%s (%s) pour org=%s (%s)",
|
||
caller.getId(), email, organisationId, org.getNom());
|
||
|
||
// ── 2. S'assurer que le caller a un lien MembreOrganisation ────────────
|
||
var lienOpt = membreOrganisationRepository
|
||
.findByMembreIdAndOrganisationId(caller.getId(), organisationId);
|
||
|
||
if (lienOpt.isEmpty()) {
|
||
LOG.infof("activerAdminOrganisation: pas de lien existant — création pour membre=%s org=%s", caller.getId(), organisationId);
|
||
membreService.lierMembreOrganisationEtIncrementerQuota(caller, organisationId, "ACTIF");
|
||
} else {
|
||
// Activer le lien existant s'il est en attente
|
||
var lien = lienOpt.get();
|
||
if (lien.getStatutMembre() != dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF) {
|
||
lien.setStatutMembre(dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF);
|
||
LOG.infof("activerAdminOrganisation: lien existant activé (statut→ACTIF) pour membre=%s", caller.getId());
|
||
}
|
||
}
|
||
|
||
// ── 3. Promouvoir en ORGADMIN (DB + Keycloak) ──────────────────────────
|
||
try {
|
||
membreService.promouvoirAdminOrganisation(caller.getId());
|
||
LOG.infof("activerAdminOrganisation: membre %s promu ORGADMIN en DB", caller.getId());
|
||
} catch (Exception e) {
|
||
LOG.errorf("activerAdminOrganisation: échec promotion DB pour membre=%s : %s", caller.getId(), e.getMessage());
|
||
}
|
||
|
||
try {
|
||
keycloakSyncService.promouvoirAdminOrganisationDansKeycloak(caller.getId());
|
||
LOG.infof("activerAdminOrganisation: rôle Keycloak ADMIN_ORGANISATION assigné pour membre=%s", caller.getId());
|
||
} catch (Exception e) {
|
||
LOG.warnf("activerAdminOrganisation: sync Keycloak ORGADMIN échouée pour membre=%s (non-bloquant): %s",
|
||
caller.getId(), e.getMessage());
|
||
}
|
||
}
|
||
|
||
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());
|
||
r.setStatut(s.getStatut() != null ? s.getStatut().name() : null);
|
||
if (s.getOrganisation() != null) {
|
||
r.setOrganisationId(s.getOrganisation().getId().toString());
|
||
r.setOrganisationNom(s.getOrganisation().getNom());
|
||
}
|
||
// ── Quota & Option C ──────────────────────────────────────────────────────
|
||
r.setQuotaMax(s.getQuotaMax());
|
||
r.setQuotaUtilise(s.getQuotaUtilise() != null ? s.getQuotaUtilise() : 0);
|
||
r.setQuotaDepasse(s.isQuotaDepasse());
|
||
if (s.getQuotaMax() != null && s.getQuotaUtilise() != null) {
|
||
r.setQuotaRestant(Math.max(0, s.getQuotaMax() - s.getQuotaUtilise()));
|
||
}
|
||
if (s.getDateFin() != null) {
|
||
r.setJoursAvantExpiration(java.time.LocalDate.now().until(s.getDateFin(), java.time.temporal.ChronoUnit.DAYS));
|
||
}
|
||
if (s.getFormule() != null) {
|
||
FormuleAbonnement f = s.getFormule();
|
||
r.setPlanCommercial(f.getPlanCommercial());
|
||
r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
|
||
r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
|
||
r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
|
||
r.setSlaGaranti(f.getSlaGaranti());
|
||
r.setMaxAdmins(f.getMaxAdmins());
|
||
r.setNiveauReporting(f.getNiveauReporting());
|
||
}
|
||
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);
|
||
// Champs Option C (V19)
|
||
r.setPlanCommercial(f.getPlanCommercial());
|
||
r.setNiveauReporting(f.getNiveauReporting());
|
||
r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
|
||
r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
|
||
r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
|
||
r.setSlaGaranti(f.getSlaGaranti());
|
||
r.setMaxAdmins(f.getMaxAdmins() != null ? f.getMaxAdmins() : -1);
|
||
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);
|
||
}
|
||
}
|