SubscriptionController.java

package com.wavii.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.wavii.model.User;
import com.wavii.model.enums.Subscription;
import com.wavii.repository.UserRepository;
import com.wavii.service.OdooService;
import com.wavii.service.StripeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/subscription")
@RequiredArgsConstructor
@Slf4j
public class SubscriptionController {

    private static final long SCHOLAR_PROMO_AMOUNT_CENTS = 299L;

    private final StripeService stripeService;
    private final UserRepository userRepository;
    private final OdooService odooService;

    private static final String STRIPE_API_VERSION = "2023-10-16";

    /**
     * Crea un SetupIntent de Stripe para preparar la recogida de un método de pago.
     * 
     * @param req Datos con el plan deseado.
     * @param principal Usuario autenticado.
     * @return 200 OK con los secretos de Stripe para el frontend.
     */
    @PostMapping("/setup-intent")
    public ResponseEntity<?> createSetupIntent(
            @RequestBody SetupIntentRequest req,
            Principal principal) {
// ... (omitted code for brevity)
        User user = getUser(principal);

        if (!stripeService.isConfigured()) {
            return ResponseEntity.ok(Map.of(
                    "ephemeralKey", "ek_test_dev_mock",
                    "setupIntentClientSecret", "seti_dev_mock_secret",
                    "customerId", "cus_dev_mock",
                    "publishableKey", "",
                    "trialUsed", user.isTrialUsed(),
                    "devMode", true
            ));
        }

        try {
            String customerId = stripeService.createOrGetCustomer(user);
            user.setStripeCustomerId(customerId);
            userRepository.save(user);

            String ephemeralKey = stripeService.createEphemeralKey(customerId, STRIPE_API_VERSION);
            Map<String, Object> siResult = stripeService.createSetupIntent(customerId, req.plan());

            return ResponseEntity.ok(Map.of(
                    "ephemeralKey", ephemeralKey,
                    "setupIntentClientSecret", siResult.get("clientSecret"),
                    "setupIntentId", siResult.get("setupIntentId"),
                    "customerId", customerId,
                    "trialUsed", user.isTrialUsed()
            ));
        } catch (Exception e) {
            log.error("Error creando SetupIntent: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al preparar el pago: " + e.getMessage()));
        }
    }

    /**
     * Confirma la suscripción tras haber completado el SetupIntent con éxito.
     * 
     * @param req Datos con el ID del SetupIntent y el plan.
     * @param principal Usuario autenticado.
     * @return 200 OK con el resultado de la suscripción.
     */
    @PostMapping("/confirm")
    public ResponseEntity<?> confirmSubscription(
            @RequestBody ConfirmSubscriptionRequest req,
            Principal principal) {

        User user = getUser(principal);

        if (!stripeService.isConfigured()) {
            Subscription sub = toSubscriptionEnum(req.plan());
            user.setSubscription(sub);
            user.setSubscriptionStatus("active");
            user.setStripeSubscriptionId("dev_sub_" + System.currentTimeMillis());
            if ("plus".equals(req.plan())) user.setTrialUsed(true);
            userRepository.save(user);
            return ResponseEntity.ok(Map.of(
                    "subscriptionId", user.getStripeSubscriptionId(),
                    "subscription", toPublicSubscriptionId(user.getSubscription()),
                    "status", "active",
                    "devMode", true
            ));
        }

        try {
            String customerId = user.getStripeCustomerId();
            Map<String, Object> result = stripeService.createSubscriptionFromSetupIntent(
                    customerId, req.setupIntentId(), req.plan(), user.isTrialUsed());

            applySubscriptionResult(user, req.plan(), result);
            userRepository.save(user);
            return ResponseEntity.ok(withPublicSubscription(result, user.getSubscription()));

        } catch (Exception e) {
            log.error("Error confirmando suscripcion: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al activar la suscripcion: " + e.getMessage()));
        }
    }

    /**
     * Inicia una suscripción directamente con un ID de método de pago.
     * 
     * @param req Datos con el plan y el ID del método de pago.
     * @param principal Usuario autenticado.
     * @return 200 OK con el resultado de la suscripción.
     */
    @PostMapping("/start")
    public ResponseEntity<?> startSubscription(
            @RequestBody StartSubscriptionRequest req,
            Principal principal) {

        User user = getUser(principal);

        if (!stripeService.isConfigured()) {
            Subscription sub = toSubscriptionEnum(req.plan());
            user.setSubscription(sub);
            user.setSubscriptionStatus("active");
            user.setStripeSubscriptionId("dev_sub_" + System.currentTimeMillis());
            if ("plus".equals(req.plan())) user.setTrialUsed(true);
            userRepository.save(user);
            return ResponseEntity.ok(Map.of(
                    "subscriptionId", user.getStripeSubscriptionId(),
                    "subscription", toPublicSubscriptionId(user.getSubscription()),
                    "status", "active",
                    "devMode", true
            ));
        }

        try {
            String customerId = stripeService.createOrGetCustomer(user);
            user.setStripeCustomerId(customerId);
            userRepository.save(user);

            Map<String, Object> result = stripeService.createSubscription(
                    customerId, req.plan(), req.paymentMethodId(), user.isTrialUsed());

            applySubscriptionResult(user, req.plan(), result);
            userRepository.save(user);
            return ResponseEntity.ok(withPublicSubscription(result, user.getSubscription()));

        } catch (Exception e) {
            log.error("Error al crear suscripcion Stripe: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al procesar el pago: " + e.getMessage()));
        }
    }

    /**
     * Programa la cancelación de la suscripción para el final del periodo actual.
     * 
     * @param principal Usuario autenticado.
     * @return 200 OK con los detalles de la cancelación.
     */
    @PostMapping("/cancel")
    public ResponseEntity<?> cancelSubscription(Principal principal) {
        User user = getUser(principal);

        if (user.getSubscription() == Subscription.FREE) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "No tienes ninguna suscripción activa que cancelar"));
        }

        if (!stripeService.isConfigured()) {
            user.setSubscriptionCancelAtPeriodEnd(true);
            user.setSubscriptionStatus("cancel_at_period_end");
            LocalDateTime currentPeriodEnd = LocalDateTime.now().plusDays(30);
            user.setSubscriptionCurrentPeriodEnd(currentPeriodEnd);
            userRepository.save(user);
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Cancelación programada de suscripción",
                    "Plan actual: " + user.getSubscription().name() + "\nFin de periodo: " + currentPeriodEnd
            );
            return ResponseEntity.ok(Map.of(
                    "cancelAtPeriodEnd", true,
                    "currentPeriodEnd", currentPeriodEnd.toString(),
                    "devMode", true
            ));
        }

        if (user.getStripeSubscriptionId() == null || user.getStripeSubscriptionId().isBlank()) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "No se encontró ninguna suscripción activa en Stripe"));
        }

        try {
            Map<String, Object> result = stripeService.cancelAtPeriodEnd(user.getStripeSubscriptionId());

            user.setSubscriptionCancelAtPeriodEnd(true);
            user.setSubscriptionStatus((String) result.getOrDefault("status", user.getSubscriptionStatus()));
            if (result.containsKey("currentPeriodEnd")) {
                user.setSubscriptionCurrentPeriodEnd(
                        LocalDateTime.parse((String) result.get("currentPeriodEnd")));
            }
            userRepository.save(user);
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Cancelación programada de suscripción",
                    "Plan actual: " + user.getSubscription().name()
                            + "\nFin de periodo: " + result.getOrDefault("currentPeriodEnd", "no informado")
            );

            log.info("Suscripción de {} programada para cancelar al final del periodo", user.getEmail());
            return ResponseEntity.ok(result);

        } catch (Exception e) {
            log.error("Error cancelando suscripcion: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al cancelar la suscripción: " + e.getMessage()));
        }
    }

    /**
     * Reactiva una suscripción que estaba programada para cancelarse.
     * 
     * @param principal Usuario autenticado.
     * @return 200 OK con los detalles de la reactivación.
     */
    @PostMapping("/reactivate")
    public ResponseEntity<?> reactivateSubscription(Principal principal) {
        User user = getUser(principal);

        if (!user.isSubscriptionCancelAtPeriodEnd()) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "Tu suscripción no está pendiente de cancelación"));
        }

        if (!stripeService.isConfigured()) {
            user.setSubscriptionCancelAtPeriodEnd(false);
            user.setSubscriptionStatus("active");
            userRepository.save(user);
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Reactivación de suscripción",
                    "Plan: " + user.getSubscription().name() + "\nEstado Stripe: active"
            );
            return ResponseEntity.ok(Map.of("cancelAtPeriodEnd", false, "devMode", true));
        }

        try {
            Map<String, Object> result = stripeService.reactivateSubscription(user.getStripeSubscriptionId());
            user.setSubscriptionCancelAtPeriodEnd(false);
            user.setSubscriptionStatus((String) result.getOrDefault("status", user.getSubscriptionStatus()));
            if (result.containsKey("currentPeriodEnd") && result.get("currentPeriodEnd") != null) {
                user.setSubscriptionCurrentPeriodEnd(LocalDateTime.parse((String) result.get("currentPeriodEnd")));
            }
            userRepository.save(user);
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Reactivación de suscripción",
                    "Plan: " + user.getSubscription().name() + "\nEstado Stripe: " + user.getSubscriptionStatus()
            );
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            log.error("Error reactivando suscripcion: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al reactivar la suscripción: " + e.getMessage()));
        }
    }

    /**
     * Cambia el plan de suscripción actual (upgrade o downgrade).
     * 
     * @param req Datos con el nuevo plan.
     * @param principal Usuario autenticado.
     * @return 200 OK con el resultado del cambio de plan.
     */
    @PostMapping("/change")
    public ResponseEntity<?> changeSubscription(
            @RequestBody ChangeSubscriptionRequest req,
            Principal principal) {

        User user = getUser(principal);

        if (user.getStripeSubscriptionId() == null || user.getStripeSubscriptionId().isBlank()) {
            return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
                    .body(Map.of(
                            "needsPaymentSheet", true,
                            "trialUsed", user.isTrialUsed(),
                            "message", "Es necesario añadir un método de pago"
                    ));
        }

        if (!stripeService.isConfigured()) {
            Subscription previousPlan = user.getSubscription();
            Subscription sub = toSubscriptionEnum(req.plan());
            boolean promoApplied = shouldApplyScholarPromo(user, req.plan());
            user.setSubscription(sub);
            user.setSubscriptionStatus("active");
            user.setSubscriptionCancelAtPeriodEnd(false);
            if (promoApplied) {
                user.setScholarPromoRedeemedAt(LocalDateTime.now());
            }
            userRepository.save(user);
            if (promoApplied) {
                odooService.processSubscriptionPlanChangePayment(
                        user.getEmail(),
                        user.getName(),
                        "Plus",
                        "Scholar",
                        SCHOLAR_PROMO_AMOUNT_CENTS / 100.0,
                        "dev_plan_change_" + System.currentTimeMillis()
                );
            }
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Cambio de suscripción: " + previousPlan.name() + " -> " + user.getSubscription().name(),
                    "Nuevo plan: " + user.getSubscription().name()
                            + "\nPromo aplicada: " + (promoApplied ? "sí" : "no")
                            + "\nEstado Stripe: active"
            );
            return ResponseEntity.ok(Map.of(
                    "subscription", toPublicSubscriptionId(user.getSubscription()),
                    "status", "active",
                    "cancelAtPeriodEnd", false,
                    "promoApplied", promoApplied,
                    "devMode", true
            ));
        }

        try {
            Subscription previousPlan = user.getSubscription();
            boolean promoApplied = shouldApplyScholarPromo(user, req.plan());
            Map<String, Object> result = stripeService.changeSubscription(
                    user.getStripeSubscriptionId(), req.plan(), promoApplied);
            Map<String, Object> promoCharge = Map.of();
            if (promoApplied) {
                promoCharge = stripeService.createImmediatePlanChangeCharge(
                        user.getStripeCustomerId(),
                        user.getStripeSubscriptionId(),
                        SCHOLAR_PROMO_AMOUNT_CENTS,
                        "Cambio Wavii Plus -> Wavii Scholar - Promo primer mes"
                );
            }

            user.setSubscription(toSubscriptionEnum(req.plan()));
            user.setSubscriptionStatus((String) result.get("status"));
            user.setSubscriptionCancelAtPeriodEnd(Boolean.TRUE.equals(result.get("cancelAtPeriodEnd")));
            if (promoApplied) {
                user.setScholarPromoRedeemedAt(LocalDateTime.now());
            }
            if (result.containsKey("currentPeriodEnd") && result.get("currentPeriodEnd") != null) {
                user.setSubscriptionCurrentPeriodEnd(
                        LocalDateTime.parse((String) result.get("currentPeriodEnd")));
            }
            userRepository.save(user);
            if (promoApplied) {
                odooService.processSubscriptionPlanChangePayment(
                        user.getEmail(),
                        user.getName(),
                        "Plus",
                        "Scholar",
                        SCHOLAR_PROMO_AMOUNT_CENTS / 100.0,
                        (String) promoCharge.get("paymentIntentId")
                );
            }
            odooService.createSubscriptionTask(
                    user.getName(),
                    user.getEmail(),
                    "Cambio de suscripción: " + previousPlan.name() + " -> " + user.getSubscription().name(),
                    "Nuevo plan: " + user.getSubscription().name()
                            + "\nPromo aplicada: " + (promoApplied ? "sí" : "no")
                            + "\nEstado Stripe: " + user.getSubscriptionStatus()
            );

            log.info("Plan de {} cambiado a {}", user.getEmail(), req.plan());
            return ResponseEntity.ok(Map.of(
                    "subscription", toPublicSubscriptionId(user.getSubscription()),
                    "subscriptionId", result.get("subscriptionId"),
                    "status", result.get("status"),
                    "cancelAtPeriodEnd", result.get("cancelAtPeriodEnd"),
                    "currentPeriodEnd", result.get("currentPeriodEnd"),
                    "promoApplied", promoApplied
            ));

        } catch (Exception e) {
            log.error("Error cambiando suscripcion: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("message", "Error al cambiar el plan: " + e.getMessage()));
        }
    }

    /**
     * Obtiene el estado detallado de la suscripción del usuario actual.
     * 
     * @param principal Usuario autenticado.
     * @return 200 OK con los datos de suscripción.
     */
    @GetMapping("/status")
    public ResponseEntity<?> getStatus(Principal principal) {
        User user = getUser(principal);
        return ResponseEntity.ok(Map.of(
                "subscription", toPublicSubscriptionId(user.getSubscription()),
                "subscriptionStatus", user.getSubscriptionStatus() != null ? user.getSubscriptionStatus() : "none",
                "stripeSubscriptionId", user.getStripeSubscriptionId() != null ? user.getStripeSubscriptionId() : "",
                "cancelAtPeriodEnd", user.isSubscriptionCancelAtPeriodEnd(),
                "currentPeriodEnd", user.getSubscriptionCurrentPeriodEnd() != null
                        ? user.getSubscriptionCurrentPeriodEnd().toString() : "",
                "trialUsed", user.isTrialUsed(),
                "deletionScheduledAt", user.getDeletionScheduledAt() != null
                        ? user.getDeletionScheduledAt().toString() : ""
        ));
    }

    /**
     * Punto de entrada para los webhooks de Stripe (eventos de pago, cancelación, etc.).
     * 
     * @param payload Cuerpo del evento enviado por Stripe.
     * @param sigHeader Cabecera de firma para validar el origen.
     * @return 200 OK.
     */
    @PostMapping("/webhook")
    @SuppressWarnings("unchecked")
    public ResponseEntity<String> handleWebhook(
            @RequestBody String payload,
            @RequestHeader(value = "Stripe-Signature", required = false) String sigHeader) {
// ... (omitted switch block for brevity)
        try {
            Event event = stripeService.constructWebhookEvent(payload, sigHeader);
            log.info("Webhook Stripe recibido: {}", event.getType());

            Map<String, Object> payloadMap = new ObjectMapper().readValue(payload, Map.class);
            Map<String, Object> data = (Map<String, Object>) payloadMap.get("data");
            Map<String, Object> obj = (Map<String, Object>) data.get("object");

            switch (event.getType()) {
                case "invoice.payment_succeeded" -> handleInvoicePaymentSucceeded(obj);
                case "customer.subscription.deleted" -> handleSubscriptionDeleted(obj);
                case "customer.subscription.updated" -> handleSubscriptionUpdated(obj);
                case "invoice.payment_failed" -> handleInvoicePaymentFailed(obj);
            }

            return ResponseEntity.ok("OK");

        } catch (SignatureVerificationException e) {
            log.error("Firma de webhook invalida: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid signature");
        } catch (Exception e) {
            log.error("Error procesando webhook: {}", e.getMessage());
            return ResponseEntity.ok("OK");
        }
    }

    /** Procesa un pago de factura realizado con éxito. */
    @SuppressWarnings("unchecked")
    private void handleInvoicePaymentSucceeded(Map<String, Object> invoice) {
        String customerId = (String) invoice.get("customer");
        Number amountPaid = (Number) invoice.get("amount_paid");
        double amount = amountPaid != null ? amountPaid.doubleValue() / 100.0 : 0.0;

        String planNameRaw = "scholar";
        try {
            Map<String, Object> lines = (Map<String, Object>) invoice.get("lines");
            List<Map<String, Object>> lineData = (List<Map<String, Object>>) lines.get("data");
            if (!lineData.isEmpty()) {
                String desc = (String) lineData.get(0).get("description");
                if (desc != null && !desc.isBlank()) planNameRaw = desc;
            }
        } catch (Exception ignored) {
        }
        final String planName = planNameRaw;

        userRepository.findByStripeCustomerId(customerId).ifPresentOrElse(user -> {
            user.setSubscriptionStatus("active");
            Subscription paidPlan = extractSubscriptionFromInvoice(invoice);
            if (paidPlan != null) {
                user.setSubscription(paidPlan);
            }
            if (user.getSubscription() == Subscription.PLUS && !user.isTrialUsed()) {
                user.setTrialUsed(true);
            }
            userRepository.save(user);
            log.info("Pago exitoso para {} - {}EUR", user.getEmail(), amount);
            odooService.processStripePayment(user.getEmail(), user.getName(), planName, amount, (String) invoice.get("id"));
        }, () -> log.warn("Webhook: no se encontro usuario con customerId={}", customerId));
    }

    /** Procesa cambios en una suscripción activa. */
    @SuppressWarnings("unchecked")
    private void handleSubscriptionUpdated(Map<String, Object> sub) {
        String customerId = (String) sub.get("customer");
        String status = (String) sub.get("status");
        Boolean cancelAtEnd = (Boolean) sub.get("cancel_at_period_end");
        Number periodEnd = (Number) sub.get("current_period_end");

        userRepository.findByStripeCustomerId(customerId).ifPresent(user -> {
            Subscription updatedPlan = extractSubscriptionFromSubscriptionObject(sub);
            if (updatedPlan != null) {
                user.setSubscription(updatedPlan);
            }
            if (status != null) user.setSubscriptionStatus(status);
            if (cancelAtEnd != null) user.setSubscriptionCancelAtPeriodEnd(cancelAtEnd);
            if (periodEnd != null) {
                user.setSubscriptionCurrentPeriodEnd(
                        LocalDateTime.ofEpochSecond(periodEnd.longValue(), 0, java.time.ZoneOffset.UTC));
            }
            userRepository.save(user);
            log.info("Suscripción actualizada para {} - status={}, cancelAtEnd={}",
                    user.getEmail(), status, cancelAtEnd);
        });
    }

    /** Procesa la eliminación/cancelación definitiva de una suscripción. */
    private void handleSubscriptionDeleted(Map<String, Object> sub) {
        String customerId = (String) sub.get("customer");
        userRepository.findByStripeCustomerId(customerId).ifPresent(user -> {
            user.setSubscription(Subscription.FREE);
            user.setSubscriptionStatus("canceled");
            user.setSubscriptionCancelAtPeriodEnd(false);
            user.setStripeSubscriptionId(null);
            userRepository.save(user);
            log.info("Suscripción eliminada para {} - bajado a FREE", user.getEmail());
        });
    }

    /** Procesa un fallo en el pago de la suscripción. */
    private void handleInvoicePaymentFailed(Map<String, Object> invoice) {
        String customerId = (String) invoice.get("customer");
        userRepository.findByStripeCustomerId(customerId).ifPresent(user -> {
            user.setSubscription(Subscription.FREE);
            user.setSubscriptionStatus("past_due");
            userRepository.save(user);
            log.warn("Pago fallido para {} - bajado a FREE", user.getEmail());
        });
    }

    /** Obtiene el usuario actual a partir del Principal. */
    private User getUser(Principal principal) {
        return userRepository.findByEmail(principal.getName())
                .orElseThrow(() -> new RuntimeException("Usuario no encontrado"));
    }

    /** Mapea el nombre del plan a su valor enum. */
    private Subscription toSubscriptionEnum(String plan) {
        return "scholar".equals(plan) ? Subscription.SCHOLAR : Subscription.PLUS;
    }

    private String toPublicSubscriptionId(Subscription subscription) {
        return subscription == null ? "free" : subscription.toPublicId();
    }

    private Map<String, Object> withPublicSubscription(Map<String, Object> result, Subscription subscription) {
        Map<String, Object> response = new HashMap<>(result);
        response.put("subscription", toPublicSubscriptionId(subscription));
        return response;
    }

    @SuppressWarnings("unchecked")
    private Subscription extractSubscriptionFromInvoice(Map<String, Object> invoice) {
        try {
            Map<String, Object> lines = (Map<String, Object>) invoice.get("lines");
            if (lines == null) return null;
            List<Map<String, Object>> lineData = (List<Map<String, Object>>) lines.get("data");
            if (lineData == null || lineData.isEmpty()) return null;
            Map<String, Object> price = (Map<String, Object>) lineData.get(0).get("price");
            String priceId = price == null ? null : (String) price.get("id");
            return subscriptionFromPriceId(priceId);
        } catch (Exception ignored) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private Subscription extractSubscriptionFromSubscriptionObject(Map<String, Object> sub) {
        try {
            Map<String, Object> items = (Map<String, Object>) sub.get("items");
            if (items == null) return null;
            List<Map<String, Object>> itemData = (List<Map<String, Object>>) items.get("data");
            if (itemData == null || itemData.isEmpty()) return null;
            Map<String, Object> price = (Map<String, Object>) itemData.get(0).get("price");
            String priceId = price == null ? null : (String) price.get("id");
            return subscriptionFromPriceId(priceId);
        } catch (Exception ignored) {
            return null;
        }
    }

    private Subscription subscriptionFromPriceId(String priceId) {
        if (priceId == null || priceId.isBlank()) return null;
        if (priceId.equals(stripeService.getPricePlus())) return Subscription.PLUS;
        if (priceId.equals(stripeService.getPriceScholar())) return Subscription.SCHOLAR;
        return null;
    }

    /** Determina si se debe aplicar la promoción Scholar (cambio desde Plus). */
    private boolean shouldApplyScholarPromo(User user, String targetPlan) {
        return "scholar".equals(targetPlan)
                && user.getSubscription() == Subscription.PLUS
                && !"trialing".equalsIgnoreCase(user.getSubscriptionStatus())
                && user.getScholarPromoRedeemedAt() == null;
    }

    /** Aplica el resultado de una operación de Stripe al estado local del usuario. */
    private void applySubscriptionResult(User user, String plan, Map<String, Object> result) {
        user.setStripeSubscriptionId((String) result.get("subscriptionId"));
        String status = (String) result.get("status");
        user.setSubscriptionStatus(status);
        if ("trialing".equals(status) || "active".equals(status)) {
            user.setSubscription(toSubscriptionEnum(plan));
        }
        if ("plus".equals(plan)) user.setTrialUsed(true);
        if (result.containsKey("cancelAtPeriodEnd")) {
            user.setSubscriptionCancelAtPeriodEnd(Boolean.TRUE.equals(result.get("cancelAtPeriodEnd")));
        }
        if (result.containsKey("currentPeriodEnd") && result.get("currentPeriodEnd") != null) {
            try {
                user.setSubscriptionCurrentPeriodEnd(
                        LocalDateTime.parse((String) result.get("currentPeriodEnd")));
            } catch (Exception ignored) {
            }
        }
    }

    record SetupIntentRequest(String plan) {}
    record ConfirmSubscriptionRequest(String plan, String setupIntentId) {}
    record StartSubscriptionRequest(String plan, String paymentMethodId) {}
    record ChangeSubscriptionRequest(String plan) {}
}