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; @Inject EmailService emailService; @ConfigProperty(name = "wave.api.url", defaultValue = "https://api.wave.com") String waveApiUrl; @ConfigProperty(name = "wave.api.key", defaultValue = "") Optional 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 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 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); try { emailService.sendPaymentConfirmationEmail( manager.getEmail(), manager.getFirstName(), establishment.getName(), sub.getAmountXof() != null ? sub.getAmountXof() : 0, sub.getPlan() != null ? sub.getPlan() : "MONTHLY" ); } catch (Exception e) { LOG.warn("Envoi email confirmation paiement échoué: " + e.getMessage()); } } } } else if ("payment.refunded".equals(eventType)) { sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED); subscriptionRepository.persist(sub); 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: remboursement traité, établissement désactivé 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); if ("payment.failed".equals(eventType)) { try { emailService.sendPaymentFailureEmail( manager.getEmail(), manager.getFirstName(), establishment.getName() ); } catch (Exception e) { LOG.warn("Envoi email échec paiement échoué: " + e.getMessage()); } } } } } }); 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.refunded".equals(eventType)) { payment.setStatus(EstablishmentPayment.STATUS_CANCELLED); } else if ("payment.failed".equals(eventType)) { payment.setStatus(EstablishmentPayment.STATUS_FAILED); } paymentRepository.persist(payment); }); } }