package com.suno.android.billing import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import arrow.core.right import com.suno.android.common_core_utils.DispatcherIO import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_data.billing.SunoBillingRepo import com.suno.android.common_data.billing.models.PeriodTransitionRule import com.suno.android.common_data.billing.models.PeriodTransitions import com.suno.android.common_data.billing.models.PlanPeriod import com.suno.android.common_data.billing.models.PlanTier import com.suno.android.common_data.billing.models.TierTransitionRule import com.suno.android.common_data.billing.models.TierTransitions import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import javax.inject.Inject /** * Validates whether a user can transition from their current subscription plan to a target plan * based on backend-defined transition rules. * * @param targetTier The desired plan tier (e.g., Pro, Premier) * @param targetPeriod The desired billing period (Month or Year) * @return either throwable if something went wrong, or a boolean indicating whether transition is allowed */ class CanSwitchToSubscriptionPlanUseCase @Inject constructor( private val billingRepo: SunoBillingRepo, @DispatcherIO private val dispatcherIO: CoroutineDispatcher, loggerFactory: SunoLogger.Factory, ) { private val logger = loggerFactory.create(this@CanSwitchToSubscriptionPlanUseCase) suspend operator fun invoke( targetTier: PlanTier, targetPeriod: PlanPeriod, ): Either = withContext(dispatcherIO) { val currentSubscription = billingRepo.getSubscriptionInfoResponse() .getOrElse { throwable -> logger.e(throwable) return@withContext throwable.left() } val currentPlan = currentSubscription.plan ?: run { // User is on basic/free plan – only upgrades allowed logger.d { "no current subscription plan, upgrade allowed" } val isUpgrade = targetTier != PlanTier.Free && targetTier != PlanTier.Basic return@withContext isUpgrade.right() } val currentTier = currentPlan.planKey?.let(PlanTier::fromString) ?: run { logger.d { "Unable to parse current plan tier: ${currentPlan.planKey}" } return@withContext IllegalStateException("Unable to parse user's current plan tier").left() } val currentPeriod = currentSubscription.period?.let(PlanPeriod::fromString) ?: run { logger.w { "Unable to parse current plan period: ${currentSubscription.period}" } return@withContext IllegalStateException("Unable to parse user's current plan period").left() } // If target plan is current plan, disallow if (currentTier == targetTier && currentPeriod == targetPeriod) { logger.d { "Target plan is current plan" } return@withContext false.right() } val rules = billingRepo.getPlanTransitionRules() .getOrElse { throwable -> logger.e(throwable) return@withContext throwable.left() } val tierRule = findValidTierTransition( from = currentTier, to = targetTier, tiers = rules.tierTransitions, ) ?: run { logger.d { "Tier transition not allowed: $currentTier -> $targetTier" } return@withContext false.right() } val periodRule = findValidPeriodTransition( from = currentPeriod, to = targetPeriod, periods = rules.periodTransitions, ) ?: run { logger.d { "Period transition not allowed: $currentPeriod -> $targetPeriod" } return@withContext false.right() } logger.d { """ Desired plan transition allowed: $currentTier->$targetTier, $currentPeriod->$targetPeriod. Valid tier rule: $tierRule Valid period rule: $periodRule """.trimIndent() } true.right() } private fun findValidTierTransition( from: PlanTier, to: PlanTier, tiers: TierTransitions, ): TierTransitionRule? = (tiers.upgrade + tiers.crossgrade) .firstOrNull { it.from == from && it.to == to } private fun findValidPeriodTransition( from: PlanPeriod, to: PlanPeriod, periods: PeriodTransitions, ): PeriodTransitionRule? { // On web, users can choose to upgrade immediately or at the end of the cycle (immediate, end_of_period). // On Android, plans are charged immediately so use "immediate". return (periods.upgrade.immediate + periods.crossgrade.immediate) .firstOrNull { it.from == from && it.to == to } } }