feat: v2.0 – réorg docker/scripts, prod, résas, abonnements Wave, Flyway base vierge

This commit is contained in:
dahoud
2026-01-29 00:44:40 +00:00
parent 9d5e388efa
commit ce89face73
66 changed files with 2333 additions and 227 deletions

View File

@@ -0,0 +1,216 @@
package com.lions.dev.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.establishment.EstablishmentPayment;
import com.lions.dev.entity.establishment.EstablishmentSubscription;
import com.lions.dev.entity.users.Users;
import com.lions.dev.repository.EstablishmentPaymentRepository;
import com.lions.dev.repository.EstablishmentRepository;
import com.lions.dev.repository.EstablishmentSubscriptionRepository;
import com.lions.dev.repository.UsersRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Service d'intégration Wave pour le paiement des droits d'accès des établissements.
* Crée une session de paiement Wave et traite les webhooks (payment.completed, etc.).
*/
@ApplicationScoped
public class WavePaymentService {
private static final Logger LOG = Logger.getLogger(WavePaymentService.class);
private static final int MONTHLY_AMOUNT_XOF = 15_000;
private static final int YEARLY_AMOUNT_XOF = 150_000;
@Inject
EstablishmentRepository establishmentRepository;
@Inject
EstablishmentSubscriptionRepository subscriptionRepository;
@Inject
EstablishmentPaymentRepository paymentRepository;
@Inject
UsersRepository usersRepository;
@ConfigProperty(name = "wave.api.url", defaultValue = "https://api.wave.com")
String waveApiUrl;
@ConfigProperty(name = "wave.api.key", defaultValue = "")
Optional<String> waveApiKey;
private final ObjectMapper objectMapper = new ObjectMapper();
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
/**
* Initie un paiement Wave pour un établissement (droits d'accès).
* Crée une session Wave et retourne l'URL de redirection.
*/
@Transactional
public InitiateSubscriptionResponseDTO initiatePayment(UUID establishmentId, String plan, String clientPhone) {
Establishment establishment = establishmentRepository.findById(establishmentId);
if (establishment == null) {
throw new IllegalArgumentException("Établissement non trouvé : " + establishmentId);
}
int amountXof = EstablishmentSubscription.PLAN_MONTHLY.equals(plan) ? MONTHLY_AMOUNT_XOF : YEARLY_AMOUNT_XOF;
String description = "AfterWork - Abonnement " + plan + " - " + establishment.getName();
EstablishmentSubscription subscription = new EstablishmentSubscription();
subscription.setEstablishmentId(establishmentId);
subscription.setPlan(plan);
subscription.setStatus(EstablishmentSubscription.STATUS_PENDING);
subscription.setAmountXof(amountXof);
subscriptionRepository.persist(subscription);
EstablishmentPayment payment = new EstablishmentPayment();
payment.setEstablishmentId(establishmentId);
payment.setAmountXof(amountXof);
payment.setStatus(EstablishmentPayment.STATUS_PENDING);
payment.setClientPhone(clientPhone);
payment.setPlan(plan);
paymentRepository.persist(payment);
String waveSessionId = null;
String paymentUrl = null;
String apiKey = waveApiKey.orElse("");
if (!apiKey.isBlank()) {
try {
Map<String, Object> body = new HashMap<>();
body.put("amount", amountXof);
body.put("currency", "XOF");
body.put("description", description);
body.put("client_reference", subscription.getId().toString());
body.put("customer_phone_number", clientPhone.startsWith("+") ? clientPhone : "+" + clientPhone);
String bodyJson = objectMapper.writeValueAsString(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(waveApiUrl + "/wave/api/v1/checkout/sessions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(15))
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
JsonNode node = objectMapper.readTree(response.body());
waveSessionId = node.has("id") ? node.get("id").asText() : null;
paymentUrl = node.has("payment_url") ? node.get("payment_url").asText() : null;
} else {
LOG.warn("Wave API error: " + response.statusCode() + " " + response.body());
}
} catch (Exception e) {
LOG.error("Erreur appel Wave API", e);
}
} else {
LOG.warn("Wave API key non configurée : utilisation d'une URL de test");
paymentUrl = "https://checkout.wave.com/session/test?ref=" + subscription.getId();
waveSessionId = "test-" + subscription.getId();
}
subscription.setWaveSessionId(waveSessionId);
payment.setWaveSessionId(waveSessionId);
subscriptionRepository.persist(subscription);
paymentRepository.persist(payment);
return new InitiateSubscriptionResponseDTO(
paymentUrl != null ? paymentUrl : "",
waveSessionId,
amountXof,
plan,
EstablishmentSubscription.STATUS_PENDING
);
}
/**
* Vérifie si un établissement a un abonnement actif (droits d'accès payés).
*/
public boolean hasActiveSubscription(UUID establishmentId) {
return subscriptionRepository.findActiveByEstablishmentId(establishmentId).isPresent();
}
/**
* Traite un webhook Wave (payment.completed, payment.cancelled, etc.).
* Met à jour l'abonnement et le paiement.
*/
@Transactional
public void handleWebhook(JsonNode payload) {
String eventType = payload.has("type") ? payload.get("type").asText() : null;
JsonNode data = payload.has("data") ? payload.get("data") : null;
if (data == null) return;
String sessionId = data.has("id") ? data.get("id").asText() : (data.has("session_id") ? data.get("session_id").asText() : null);
if (sessionId == null) return;
subscriptionRepository.findByWaveSessionId(sessionId).ifPresent(sub -> {
UUID establishmentId = sub.getEstablishmentId();
Establishment establishment = establishmentRepository.findById(establishmentId);
if ("payment.completed".equals(eventType)) {
sub.setStatus(EstablishmentSubscription.STATUS_ACTIVE);
sub.setPaidAt(LocalDateTime.now());
if (EstablishmentSubscription.PLAN_MONTHLY.equals(sub.getPlan())) {
sub.setExpiresAt(LocalDateTime.now().plusMonths(1));
} else {
sub.setExpiresAt(LocalDateTime.now().plusYears(1));
}
subscriptionRepository.persist(sub);
// Activer l'établissement et le manager
if (establishment != null) {
establishment.setIsActive(true);
establishmentRepository.persist(establishment);
Users manager = establishment.getManager();
if (manager != null) {
manager.setActive(true);
usersRepository.persist(manager);
LOG.info("Webhook Wave: établissement et manager activés pour " + establishmentId);
}
}
} else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)
|| "payment.failed".equals(eventType)) {
sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED);
subscriptionRepository.persist(sub);
// Suspendre l'établissement et le manager
if (establishment != null) {
establishment.setIsActive(false);
establishmentRepository.persist(establishment);
Users manager = establishment.getManager();
if (manager != null) {
manager.setActive(false);
usersRepository.persist(manager);
LOG.info("Webhook Wave: établissement et manager suspendus pour " + establishmentId);
}
}
}
});
paymentRepository.findByWaveSessionId(sessionId).ifPresent(payment -> {
if ("payment.completed".equals(eventType)) {
payment.setStatus(EstablishmentPayment.STATUS_COMPLETED);
} else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)) {
payment.setStatus(EstablishmentPayment.STATUS_CANCELLED);
}
paymentRepository.persist(payment);
});
}
}