Aerendir/bundle-features

View on GitHub
src/Manager/FeaturesManager.php

Summary

Maintainability
F
3 days
Test Coverage
<?php

declare(strict_types=1);

/*
 * This file is part of the Serendipity HQ Features Bundle.
 *
 * Copyright (c) Adamo Aerendir Crespi <aerendir@serendipityhq.com>.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SerendipityHQ\Bundle\FeaturesBundle\Manager;

use SerendipityHQ\Bundle\FeaturesBundle\Form\DataTransformer\FeaturesCollectionTransformer;
use SerendipityHQ\Bundle\FeaturesBundle\Form\Type\FeaturesType;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Configured\ConfiguredBooleanFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Configured\ConfiguredCountableFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Configured\ConfiguredFeaturesCollection;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Configured\ConfiguredRechargeableFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\FeatureInterface;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Property\CanBeConsumedInterface;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Property\CanBeEnabledInterface;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Property\IsRecurringFeatureInterface;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Subscribed\SubscribedBooleanFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Subscribed\SubscribedCountableFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Subscribed\SubscribedCountableFeaturePack;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Subscribed\SubscribedFeaturesCollection;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\Subscribed\SubscribedRechargeableFeature;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Subscription;
use SerendipityHQ\Bundle\FeaturesBundle\Model\SubscriptionInterface;
use SerendipityHQ\Component\ValueObjects\Money\Money;
use SerendipityHQ\Component\ValueObjects\Money\MoneyInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;

use function Safe\sprintf;

/**
 * Contains method to manage features plans.
 */
final class FeaturesManager
{
    private const ADDED   = 'added';
    private const REMOVED = 'removed';

    /** @var int[] */
    private const INTERVALS = [
        SubscriptionInterface::DAILY    => 0,
        SubscriptionInterface::WEEKLY   => 1,
        SubscriptionInterface::BIWEEKLY => 2,
        SubscriptionInterface::MONTHLY  => 3,
        SubscriptionInterface::YEARLY   => 4,
    ];

    private ConfiguredFeaturesCollection $configuredFeatures;
    private FormFactoryInterface $formFactory;
    private InvoicesManager $invoicesManager;
    private SubscriptionInterface $subscription;
    private SubscriptionInterface $oldSubscription;

    /** @var array $differences The added and removed features */
    private $differences = [
        self::ADDED   => [],
        self::REMOVED => [],
    ];

    public function __construct(array $configuredFeatures, InvoicesManager $invoicesManager, FormFactoryInterface $formFactory)
    {
        $this->configuredFeatures = new ConfiguredFeaturesCollection($configuredFeatures);
        $this->formFactory        = $formFactory;
        $this->invoicesManager    = $invoicesManager;
    }

    /**
     * Returns all the configured features.
     */
    public function getConfiguredFeatures(): ConfiguredFeaturesCollection
    {
        return $this->configuredFeatures;
    }

    public function getSubscription(): SubscriptionInterface
    {
        return $this->subscription;
    }

    public function setSubscription(SubscriptionInterface $subscription): self
    {
        $this->subscription = $subscription;
        $this->getInvoicesManager()->setSubscription($this->getSubscription());
        $this->getConfiguredFeatures()->setSubscription($this->getSubscription());

        /**
         * Set the Configured feature in each subscribed feature.
         */
        foreach ($subscription->getFeatures()->getValues() as $subscribedFeature) {
            $configuredFeature = $this->getConfiguredFeatures()->get($subscribedFeature->getName());

            // If the feature doesn't exist anymore in configuration, skip it: it will be deleted on next subscription update
            if (null !== $configuredFeature) {
                $subscribedFeature->setConfiguredFeature($configuredFeature);
            }
        }

        return $this;
    }

    public function setTax(float $rate, string $name): self
    {
        $this->getConfiguredFeatures()->setTax($rate, $name);
        $this->getInvoicesManager()->getConfiguredFeatures()->setTax($rate, $name);

        return $this;
    }

    public function buildDefaultSubscriptionFeatures(string $subscriptionInterval): SubscribedFeaturesCollection
    {
        $activeUntil = Subscription::calculateActiveUntil($subscriptionInterval);
        $features    = [];

        foreach ($this->getConfiguredFeatures() as $name => $details) {
            switch ($details->getType()) {
                case FeatureInterface::TYPE_BOOLEAN:
                    /** @var ConfiguredBooleanFeature $details */
                    $features[$name] = [
                        FeatureInterface::FIELD_TYPE                    => $details->getType(),
                        CanBeEnabledInterface::FIELD_ENABLED            => $details->isEnabled(),
                        IsRecurringFeatureInterface::FIELD_ACTIVE_UNTIL => false === $this->getConfiguredFeatures()->get($name)->isEnabled() ? null : $activeUntil,
                    ];

                    break;
                case FeatureInterface::TYPE_COUNTABLE:
                    /** @var ConfiguredCountableFeature $details */
                    $features[$name] = [
                        FeatureInterface::FIELD_TYPE                      => $details->getType(),
                        SubscribedCountableFeature::FIELD_SUBSCRIBED_PACK => [SubscribedCountableFeature::FIELD_SUBSCRIBED_NUM_OF_UNITS => $this->getConfiguredFeatures()->get($name)->getFreePack()->getNumOfUnits()],
                        CanBeConsumedInterface::REMAINED_QUANTITY         => $this->getConfiguredFeatures()->get($name)->getFreePack()->getNumOfUnits(),
                    ];

                    break;
                case FeatureInterface::TYPE_RECHARGEABLE:
                    /** @var ConfiguredRechargeableFeature $details */
                    $features[$name] = [
                        FeatureInterface::FIELD_TYPE              => $details->getType(),
                        'last_recharge_on'                        => new \DateTime(),
                        'last_recharge_quantity'                  => $this->getConfiguredFeatures()->get($name)->getFreeRecharge(),
                        CanBeConsumedInterface::REMAINED_QUANTITY => $this->getConfiguredFeatures()->get($name)->getFreeRecharge(),
                    ];

                    break;
            }
        }

        return new SubscribedFeaturesCollection($features);
    }

    /**
     * @param SubscribedFeaturesCollection $newFeatures This comes from the form, not from the Subscription! The Subscription is
     *                                                  not yet synced with these new Features!
     */
    public function calculateTotalChargesForNewFeatures(SubscribedFeaturesCollection $newFeatures): MoneyInterface
    {
        $totalCharges = new Money([MoneyInterface::BASE_AMOUNT => 0, MoneyInterface::CURRENCY => $this->getSubscription()->getCurrency()]);

        // Calculate the added and removed Boolean features and the changed packages in Countable features
        $this->findDifferences($newFeatures);

        /*
         * May happen that a premium feature is activate and paid, then is deactivated but it is still in the subscription interval.
         * If it is activated again during the subscription interval, it were already paid, so it hasn't to be paid again.
         */
        foreach ($this->getDifferences(self::ADDED) as $feature) {
            $featureName = \is_array($feature) ? \key($feature) : $feature;
            /** @var SubscribedBooleanFeature|SubscribedCountableFeature|SubscribedRechargeableFeature $checkingFeature */
            $checkingFeature = $this->getSubscription()->getFeatures()->get($featureName);

            if (null !== $checkingFeature) {
                /** @var ConfiguredBooleanFeature|ConfiguredCountableFeature|ConfiguredRechargeableFeature $configuredFeature */
                $configuredFeature = $this->getConfiguredFeatures()->get($featureName);
                $price             = null;

                switch (\get_class($checkingFeature)) {
                    // These two have recurring features, so they can or cannot be still active
                    case SubscribedBooleanFeature::class:
                        if (true === $checkingFeature->isStillActive()) {
                            // If it is still active, we have to charge nothing, so continue processing next feature
                            // continue, // Here there was a continue, but Rector reports a PHP warning
                            // PHP Warning:  "continue" targeting switch is equivalent to "break". Did you mean to use "continue 2"?
                            // When you read this comment, evaluate if the bundle continues to work as expected or not.
                            // If it continues to work as expected, remove this entire comment.
                            break;
                        }

                        $price = $configuredFeature->getInstantPrice($this->getSubscription()->getCurrency(), $this->getSubscription()->getRenewInterval(), FeatureInterface::PRICE_GROSS);

                        break;
                    case SubscribedCountableFeature::class:
                        // @todo Support unitary_prices for CountableFeatures https://github.com/Aerendir/bundle-features/issues/1
                        if ($configuredFeature instanceof ConfiguredCountableFeature) {
                            /**
                             * For the moment force the code to get the pack's instant price.
                             *
                             * @var SubscribedCountableFeature $price
                             */
                            $price = $configuredFeature->getPack($checkingFeature->getSubscribedPack()->getNumOfUnits())->getInstantPrice($this->getSubscription()->getCurrency(), $this->getSubscription()->getRenewInterval(), FeatureInterface::PRICE_GROSS);
                        }

                        break;
                        // A RechargeableFeature hasn't a subscription period, so it hasn't an isStillActive() method
                    case SubscribedRechargeableFeature::class:
                        /**
                         * For the moment force the code to get the pack's instant price.
                         */
                        $price = $configuredFeature->getPack($checkingFeature->getRechargingPack()->getNumOfUnits())->getPrice($this->getSubscription()->getCurrency(), FeatureInterface::PRICE_GROSS);

                        break;
                }

                if ($price instanceof MoneyInterface) {
                    $totalCharges = $totalCharges->add($price);
                }
            }
        }

        return $totalCharges;
    }

    public function getDifferences(string $type = null): array
    {
        if (null === $this->differences) {
            throw new \LogicException('No differences calculated. You have to first call findDifferences().');
        }

        if (self::ADDED !== $type && self::REMOVED !== $type && null !== $type) {
            throw new \InvalidArgumentException(sprintf('You can only get "added" or "removed" differences or all passing "null". You asked for "%s".', $type));
        }

        return null === $type ? $this->differences : $this->differences[$type];
    }

    public function getFeaturesFormBuilder(string $actionUrl, SubscriptionInterface $subscription): FormBuilderInterface
    {
        // Generate this form only once
        static $form = null;

        if (null === $form) {
            // Set the subscription in the manager if not already done outside of the bundle by the implementing app
            // Here we assume this features manager is used ever with the same subscription
            if (null === $this->subscription) {
                $this->setSubscription($subscription);
            }

            if (false === $this->getConfiguredFeatures()->isTaxSet()) {
                throw new \RuntimeException("To generate a valid form you have to set a Tax. Call first setTax() and then you'll can call getFeaturesFormBuilder(). Ex.: FeaturesManager::setTax()->getFeaturesFormBuilder()");
            }

            // Clone the $subscription so we can use it to compare changes
            $this->oldSubscription = clone $subscription;

            $form = $this->formFactory->createBuilder(FormType::class, [
                'action' => $actionUrl,
                'method' => 'POST',
            ])
                ->add('features', FeaturesType::class, [
                    'data'                => $subscription->getFeatures()->toArray(),
                    'configured_features' => $this->getConfiguredFeatures(),
                    'subscription'        => $subscription,
                ]);

            $form->get('features')->addModelTransformer(new FeaturesCollectionTransformer());
        }

        return $form;
    }

    /**
     * Returns the premium features activated and not activated.
     *
     * Given a Subscription object, intersect it with configured features.
     * Of configured features, this takes care only of the Premium ones (the ones that has at least one price set).
     * So, if a Premium configured feature is not present in the given Subscription, it is set as not enabled, while, if
     * it exists in the given Subscription, it has its same status (if enabled in the Subscription, it will be enabled,
     * disabled instead).
     *
     * @todo Method to implement
     */
    public function getPremiumFeaturesReview(): ConfiguredFeaturesCollection
    {
        return $this->getConfiguredFeatures();
    }

    /**
     * Reverts the Subscription to the state before the editings.
     */
    public function rollbackSubscription(): void
    {
        $this->subscription
            ->setCurrency($this->oldSubscription->getCurrency())
            ->setFeatures($this->oldSubscription->getFeatures())
            ->setRenewInterval($this->oldSubscription->getRenewInterval())
            ->setNextRenewAmount($this->oldSubscription->getNextRenewAmount())
            ->setNextRenewOn($this->oldSubscription->getNextRenewOn());
    }

    public function syncSubscription(SubscriptionInterface $subscription, SubscribedFeaturesCollection $features): void
    {
        foreach ($features as $featureName => $feature) {
            $toggle = $feature->isEnabled() ? 'enable' : 'disable';
            $subscription->getFeatures()->get($featureName)->$toggle();
        }
    }

    /**
     * Update the subscription object after features are added or removed.
     *
     * It updates the next payment amount and the dates until the features are active.
     */
    public function updateSubscription(SubscribedFeaturesCollection $newFeatures = null): void
    {
        if (null !== $newFeatures) {
            /**
             * Before all, update the features, setting the new enabled status or adding the feature if not already present.
             */
            foreach ($newFeatures as $newFeature) {
                $existentFeature = $this->getSubscription()->getFeatures()->get($newFeature->getName());

                // @todo Is this required? Didn't the form already updated the Subscription object? In fact I have an oldSubscription
                if ($existentFeature instanceof SubscribedBooleanFeature) {
                    $toggle = $newFeature->isEnabled() ? 'enable' : 'disable';
                    $existentFeature->$toggle();
                }

                if (false === $this->getSubscription()->has($newFeature->getName())) {
                    $this->getSubscription()->addFeature($newFeature->getName(), $newFeature);
                }
            }
        }

        $this->updateNextPaymentAmount();
        $this->refreshCountableFeatures();
        $this->updateUntilDates();
    }

    /**
     * Renews the countable features at the end of the renew period.
     */
    public function refreshSubscription(): void
    {
        $subscription = $this->getSubscription();

        /** @var FeatureInterface $feature */
        foreach ($subscription->getFeatures()->getValues() as $feature) {
            // If this is not a Countable Feature...
            if ( ! $feature instanceof SubscribedCountableFeature) {
                // Simply continue as it hasn't be renew
                continue;
            }

            /** @var ConfiguredCountableFeature $configuredRenewingFeature Get the configured feature * */
            $configuredRenewingFeature = $this->getConfiguredFeatures()->get($feature->getName());

            // If the feature doesn't exist anymore in the configuration (as it were removed by the developer)
            if (null === $configuredRenewingFeature) {
                // Remove it from the Subscription too
                $subscription->getFeatures()->removeElement($feature);

                // And continue with the next feature
                continue;
            }

            /** @var SubscribedCountableFeature $feature Refresh the feature if the refresh period is elapsed * */
            if ($feature->isRefreshPeriodElapsed()) {
                $feature->refresh();
            }
        }

        $this->refreshCountableFeatures();

        $subscription->forceFeaturesUpdate();
    }

    public function getInvoicesManager(): InvoicesManager
    {
        return $this->invoicesManager;
    }

    private function calculateSubscriptionAmount(): MoneyInterface
    {
        $total = new Money([MoneyInterface::BASE_AMOUNT => 0, MoneyInterface::CURRENCY => $this->getSubscription()->getCurrency()]);

        /** @var FeatureInterface $feature */
        foreach ($this->getSubscription()->getFeatures() as $feature) {
            // Check if the feature is still present in configuration
            if (null === $this->getConfiguredFeatures()->get($feature->getName())) {
                // It is not present anymore: remove it from the subscription
                $this->getSubscription()->getFeatures()->remove($feature->getName());

                // Simply Continue the cycle
                continue;
            }

            if ($feature instanceof SubscribedBooleanFeature && $feature->isEnabled()) {
                $price = $this->getConfiguredFeatures()->get($feature->getName())->getPrice($this->getSubscription()->getCurrency(), $this->getSubscription()->getRenewInterval());

                if ($price instanceof MoneyInterface) {
                    $total = $total->add($price);
                }
            }
        }

        return $total;
    }

    /**
     * Calculate differences between two FeaturesCollections.
     *
     * Calculates the added and removed features in the $newFeatures comparing it with $oldFeatures
     *
     * @return array
     */
    private function findDifferences(SubscribedFeaturesCollection $newFeatures)
    {
        // Get the features from the cloned subscription
        $oldFeatures = $this->oldSubscription->getFeatures();

        /**
         * Calculate the removed features.
         *
         * A feature is removed if:
         * 1. It was in the old collection but doesn't exist in the new collection;
         * 2. It was in the old collection and was enabled and is in the new collection but is not enabled
         */
        foreach ($oldFeatures as $oldFeature) {
            // If the Feature is in the old collection but doesn't exist in the new collection...
            if (false === $newFeatures->containsKey($oldFeature->getName())) {
                // ... It was removed and in this case we can simply set it as removed as we don't need much details
                $this->differences[self::REMOVED][] = $oldFeature->getName();

                continue;
            }

            switch (\get_class($oldFeature)) {
                // If is a BooleanFeature...
                case SubscribedBooleanFeature::class:
                    /** @var SubscribedBooleanFeature $oldFeature */
                    // ... and was in the old collection and was enabled and is in the new collection but is not enabled...
                    if (true === $oldFeature->isEnabled()
                        && true === $newFeatures->containsKey($oldFeature->getName())
                        && false === $newFeatures->get($oldFeature->getName())->isEnabled()
                    ) {
                        // ... It was removed
                        $this->differences[self::REMOVED][] = $oldFeature->getName();
                    }

                    break;
                    // If is a CountableFeature...
                case SubscribedCountableFeature::class:
                    /**
                     * ... and was in the old collection and in the new collection, too ...
                     */
                    if (true === $newFeatures->containsKey($oldFeature->getName())) {
                        /**
                         * We first get the subscribed packages...
                         */
                        $oldSubscribedPack = $oldFeature->getSubscribedPack();
                        $newSubscribedPack = $newFeatures->get($oldFeature->getName())->getSubscribedPack();

                        // ... and then we compare them. If they are not equal...
                        if ($oldSubscribedPack->getNumOfUnits() !== $newSubscribedPack->getNumOfUnits()) {
                            // ... the pack was removed (changed)
                            $this->differences[self::REMOVED][] = [$oldFeature->getName() => $oldSubscribedPack->getNumOfUnits()];
                        }
                    }

                    break;
                case SubscribedRechargeableFeature::class:
                    break;
            }
        }

        /**
         * Calculate the added features.
         *
         * A feature is added if:
         * 1. It was not in the old collection but exists in the new collection;
         * 2. It was in the old collection and was not enabled and is in the new collection too but is enabled
         */
        foreach ($newFeatures as $newFeature) {
            /*
             * Here we first build the value to add as we need to distinguish immediately between the two kind of
             * features, boolean and countable, because if a CountableFeature is added, we need to know the subscribed
             * plan.
             */
            $featureDetails = '';
            switch (\get_class($newFeature)) {
                // If is a BooleanFeature...
                case SubscribedBooleanFeature::class:
                    // ... we simply need its name
                    $featureDetails = $newFeature->getName();

                    break;
                    // If is a CountableFeature...
                case SubscribedCountableFeature::class:
                    /** @var SubscribedCountableFeature $featureDetails */
                    $featureDetails = [$newFeature->getName() => $newFeature->getSubscribedPack()->getNumOfUnits()];

                    break;
                    // If is a CountableFeature...
                case SubscribedRechargeableFeature::class:
                    /** @var SubscribedRechargeableFeature $featureDetails */
                    $featureDetails = [$newFeature->getName() => $newFeature->getRechargingPack()->getNumOfUnits()];

                    break;
            }

            // ... If the feature was not in the old collection but exists in the new collection...
            if (false === $oldFeatures->containsKey($newFeature->getName())) {
                // ... It was added for sure
                $this->differences[self::ADDED][] = $featureDetails;

                continue;
            }

            // If the new feature already was in the old collection...
            if (true === $oldFeatures->containsKey($newFeature->getName())) {
                // We need to know which kind of feature we are checking to know how to do the check
                switch (\get_class($newFeature)) {
                    // If is a BooleanFeature...
                    case SubscribedBooleanFeature::class:
                        // If now, in the new subscription, is enabled...
                        if (true === $newFeature->isEnabled()
                            // ... But were not enabled in the old subscription
                            && false === $oldFeatures->get($newFeature->getName())->isEnabled()
                        ) {
                            // ... then, it was added
                            $this->differences[self::ADDED][] = $featureDetails;
                        }

                        break;
                        // If is a CountableFeature...
                    case SubscribedCountableFeature::class:
                        /** @var SubscribedCountableFeaturePack $newSubscribedPack */
                        $newSubscribedPack = $newFeature->getSubscribedPack();

                        /** @var SubscribedCountableFeaturePack $oldSubscribedPack */
                        $oldSubscribedPack = $oldFeatures->get($newFeature->getName())->getSubscribedPack();

                        // We first get the subscribed packages and then we compare them. If they are not equal...
                        if ($oldSubscribedPack->getNumOfUnits() !== $newSubscribedPack->getNumOfUnits()) {
                            // ... the pack was removed (changed)
                            $this->differences[self::ADDED][] = $featureDetails;
                        }

                        break;
                        // If it is a RechargeableFeature...
                    case SubscribedRechargeableFeature::class:
                        // ... if a rechargin pack exists...
                        if ($newFeature->hasRechargingPack()) {
                            // ... We are simply recharging the feature
                            $this->differences[self::ADDED][] = $featureDetails;
                        }

                        break;
                }
            }
        }

        return $this->getDifferences();
    }

    /**
     * Updates the amount of the next payment for the provided subscription object.
     */
    private function updateNextPaymentAmount(): void
    {
        $this->getSubscription()->setNextRenewAmount($this->calculateSubscriptionAmount());
    }

    /**
     * Updates the renew period based on Countable features present (to the smallest interval) and sets the next renew
     * date.
     */
    private function refreshCountableFeatures(): void
    {
        $refreshInterval = SubscriptionInterface::MONTHLY;
        /** @var SubscribedCountableFeature $feature */
        foreach ($this->getSubscription()->getFeatures()->getValues() as $feature) {
            if ($feature instanceof SubscribedCountableFeature) {
                /** @var ConfiguredCountableFeature $configuredFeature */
                $configuredFeature = $feature->getConfiguredFeature();

                // If the configured renew period is smaller than the current renew period...
                if (self::INTERVALS[$configuredFeature->getRefreshPeriod()] < self::INTERVALS[$refreshInterval]) {
                    // Set the configured renew period as the new current renew period
                    $refreshInterval = $configuredFeature->getRefreshPeriod();
                }

                // Refresh the feature
                $feature->refresh();
            }
        }

        $nextRefreshOn = $this->getSubscription()->getNextRefreshOn() ?? clone $this->getSubscription()->getSubscribedOn();
        $this->getSubscription()
            ->setSmallestRefreshInterval($refreshInterval)
            ->setNextRefreshOn($nextRefreshOn);
        switch ($this->getSubscription()->getSmallestRefreshInterval()) {
            // We need to clone the \DateTime object to change its reference
            // @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/working-with-datetime.html
            case SubscriptionInterface::DAILY:
                $this->getSubscription()->setNextRefreshOn(
                    clone $this->getSubscription()->getNextRefreshOn()->modify('+1 day')
                );

                break;
            case SubscriptionInterface::WEEKLY:
                $this->getSubscription()->setNextRefreshOn(
                    clone $this->getSubscription()->getNextRefreshOn()->modify('+1 week')
                );

                break;
            case SubscriptionInterface::BIWEEKLY:
                $this->getSubscription()->setNextRefreshOn(
                    clone $this->getSubscription()->getNextRefreshOn()->modify('+2 week')
                );

                break;
            case SubscriptionInterface::MONTHLY:
                $this->getSubscription()->setNextRefreshOn(
                    clone $this->getSubscription()->getNextRefreshOn()->modify('+1 month')
                );

                break;
            case SubscriptionInterface::YEARLY:
                $this->getSubscription()->setNextRefreshOn(
                    clone $this->getSubscription()->getNextRefreshOn()->modify('+1 year')
                );

                break;
        }
    }

    /**
     * Updates the date until the features in the Subscription are active.
     */
    private function updateUntilDates(): void
    {
        $validUntil = $this->getSubscription()->getNextRenewOn();

        /** @var array $feature */
        foreach ($this->getDifferences(self::ADDED) as $feature) {
            // If this is an array, this is a Package...
            if (\is_array($feature)) {
                // So we need the key of the array that is the feature's name
                $feature = \key($feature);
            }

            if (false === $this->getSubscription()->has($feature)) {
                $this->getSubscription()->addFeature(
                    $feature, $this->getConfiguredFeatures()->get($feature)
                );
            }

            /** @var FeatureInterface $updatingFeature */
            $updatingFeature = $this->getSubscription()->getFeatures()->get($feature);

            if ($updatingFeature instanceof IsRecurringFeatureInterface) {
                $updatingFeature->setActiveUntil($validUntil);
            }
        }
    }
}