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:
dahoud
2026-04-04 16:14:30 +00:00
parent 9c66909eff
commit e00a9301d8
98 changed files with 5571 additions and 636 deletions

View File

@@ -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);
}
}