Aerendir/bundle-features

View on GitHub
src/Model/Feature/Property/HasRecurringPricesProperty.php

Summary

Maintainability
B
6 hrs
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\Model\Feature\Property;

use Money\Currency;
use SerendipityHQ\Bundle\FeaturesBundle\Model\Feature\FeatureInterface;
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 function Safe\sprintf;

/**
 * Common properties and methods of a ConfiguredRecurringFeatureInterface.
 *
 * @method isFromConfiguration() Is contained in AbstractFeature
 */
trait HasRecurringPricesProperty
{
    /** @var array $instantGrossPrices */
    private $instantGrossPrices;

    /** @var array $instantNetPrices */
    private $instantNetPrices;

    /** @var array $grossPrices */
    private $grossPrices;

    /** @var array $netPrices */
    private $netPrices;

    /** @var string $pricesType */
    private $pricesType;

    /** @var SubscriptionInterface $subscription */
    private $subscription;

    /** @var string $taxName */
    private $taxName;

    /** @var float $taxRate */
    private $taxRate;

    public function __construct(array $details = [])
    {
        $this->instantGrossPrices = [];
        $this->instantNetPrices   = [];
        $this->grossPrices        = [];
        $this->netPrices          = [];
        if (isset($details[HasRecurringPricesInterface::FIELD_NET_PRICES])) {
            $this->setPrices($details[HasRecurringPricesInterface::FIELD_NET_PRICES], FeatureInterface::PRICE_NET);
        }

        if (isset($details[HasRecurringPricesInterface::FIELD_GROSS_PRICES])) {
            $this->setPrices($details[HasRecurringPricesInterface::FIELD_GROSS_PRICES], FeatureInterface::PRICE_GROSS);
        }
    }

    /**
     * @param Currency|string $currency This is not typecasted so the method can be called from inside Twig templates simply passing a string
     *
     * @return MoneyInterface|null if the price is not set in the required currency
     */
    public function getInstantPrice($currency, string $subscriptionInterval, string $type = null): ?MoneyInterface
    {
        if ($currency instanceof Currency) {
            $currency = $currency->getCode();
        }

        if (null === $type) {
            $type = $this->pricesType;
        }

        $instantPricesProperty = FeatureInterface::PRICE_NET === $type ? 'instantNetPrices' : 'instantGrossPrices';
        $instantPrices         = $this->$instantPricesProperty;

        if (null !== $instantPrices && false === \is_array($instantPrices)) {
            throw new \RuntimeException('Something went wrong returning instant prices.');
        }

        if (false === isset($instantPrices[$currency][$subscriptionInterval])) {
            $instantPrices[$currency][$subscriptionInterval] = $this->calculateInstantPrice($currency, $subscriptionInterval, $type);
            $this->$instantPricesProperty                    = $instantPrices;
        }

        return $instantPrices[$currency][$subscriptionInterval] ?? null;
    }

    /**
     * @param Currency|string $currency This is not typecasted so the method can be called from inside Twig templates simply passing a string
     */
    public function getPrice($currency, string $subscriptionInterval, string $type = null): MoneyInterface
    {
        if ($currency instanceof Currency) {
            $currency = $currency->getCode();
        }

        Subscription::checkIntervalExists($subscriptionInterval);

        if (null === $type) {
            $type = $this->pricesType;
        }

        return $this->getPrices($type)[$currency][$subscriptionInterval] ?? new Money([MoneyInterface::BASE_AMOUNT => 0, MoneyInterface::CURRENCY => new Currency($currency)]);
    }

    public function getPrices(string $type = null): ?array
    {
        if (null === $type) {
            $type = $this->pricesType;
        }

        switch ($type) {
            case FeatureInterface::PRICE_GROSS:
                return $this->grossPrices;
            case FeatureInterface::PRICE_NET:
                return $this->netPrices;
            default:
                throw new \InvalidArgumentException(sprintf('The prices can be only "net" or "gross". You asked for "%s" prices.', $type));
        }
    }

    public function getTaxName(): string
    {
        return $this->taxName;
    }

    public function getTaxRate(): float
    {
        return $this->taxRate;
    }

    /**
     * @param Currency|string $currency This is not typecasted so the method can be called from inside Twig templates simply passing a string
     */
    public function hasPrice($currency, string $subscriptionInterval, string $type = null): bool
    {
        Subscription::checkIntervalExists($subscriptionInterval);

        if ($currency instanceof Currency) {
            $currency = $currency->getCode();
        }

        if (null === $type) {
            $type = $this->pricesType;
        }

        return isset($this->getPrices($type)[$currency][$subscriptionInterval]);
    }

    public function setSubscription(SubscriptionInterface $subscription): HasRecurringPricesInterface
    {
        $this->subscription = $subscription;

        /** @var HasRecurringPricesInterface $this */
        return $this;
    }

    public function setTax(float $rate, string $name): HasRecurringPricesInterface
    {
        $this->taxName = $name;
        $this->taxRate = $rate;

        $pricesProperty = FeatureInterface::PRICE_NET === $this->pricesType ? 'netPrices' : 'grossPrices';
        // ... Then we have to set gross prices
        if (\is_countable($this->$pricesProperty) && 0 < \count($this->$pricesProperty)) {
            foreach ($this->$pricesProperty as $currency => $prices) {
                /** @var MoneyInterface $price */
                foreach ($prices as $subscriptionInterval => $price) {
                    switch ($this->pricesType) {
                        // If currently is "net"...
                        case FeatureInterface::PRICE_NET:
                            $netPrice                                            = (int) \round($price->getBaseAmount() * (1 + $rate));
                            $netPrice                                            = new Money([MoneyInterface::BASE_AMOUNT => $netPrice, MoneyInterface::CURRENCY => $currency]);
                            $this->grossPrices[$currency][$subscriptionInterval] = $netPrice;

                            break;
                            // If currently is "gross"...
                        case FeatureInterface::PRICE_GROSS:
                            // ... Then we have to set net prices
                            $grossPrice                                        = (int) \round($price->getBaseAmount() / (1 + $rate));
                            $grossPrice                                        = new Money([MoneyInterface::BASE_AMOUNT => $grossPrice, MoneyInterface::CURRENCY => $currency]);
                            $this->netPrices[$currency][$subscriptionInterval] = $grossPrice;

                            break;
                    }
                }
            }
        }

        /** @var HasRecurringPricesInterface $this */
        return $this;
    }

    private function setPrices(array $settingPrices, string $pricesType)
    {
        $this->pricesType = $pricesType;
        $priceProperty    = FeatureInterface::PRICE_NET === $this->pricesType ? 'netPrices' : 'grossPrices';
        $setPrices        = $this->$priceProperty;

        if ([] !== $settingPrices) {
            foreach ($settingPrices as $currency => $price) {
                $currency = new Currency($currency);

                if (isset($price[SubscriptionInterface::MONTHLY])) {
                    $amount = $price[SubscriptionInterface::MONTHLY];
                    if ( ! $amount instanceof MoneyInterface) {
                        $amount = new Money([
                            MoneyInterface::BASE_AMOUNT => $price[SubscriptionInterface::MONTHLY], MoneyInterface::CURRENCY => $currency,
                        ]);
                    }

                    $setPrices[$currency->getCode()][SubscriptionInterface::MONTHLY] = $amount;
                }

                if (isset($price[SubscriptionInterface::YEARLY])) {
                    $amount = $price[SubscriptionInterface::YEARLY];
                    if ( ! $amount instanceof MoneyInterface) {
                        $amount = new Money([
                            MoneyInterface::BASE_AMOUNT => $price[SubscriptionInterface::YEARLY], MoneyInterface::CURRENCY => $currency,
                        ]);
                    }

                    $setPrices[$currency->getCode()][SubscriptionInterface::YEARLY] = $amount;
                }
            }
        }

        $this->$priceProperty = $setPrices;
    }

    private function calculateInstantPrice(string $currency, string $subscriptionInterval, string $pricesType): MoneyInterface
    {
        if ( ! $this->subscription instanceof SubscriptionInterface) {
            throw new \RuntimeException('Before you can get instant prices you have to set a Subscription with setSubscription().');
        }

        $price = $this->getPrice($currency, $subscriptionInterval, $pricesType);

        // If the feature is not already subscribed or if it was subscribed today
        if (null === $this->subscription->getSubscribedOn() || ($this->subscription->getSubscribedOn()->format('Y-m-d') === (new \DateTime())->format('Y-m-d'))) {
            // ...the user has never paid, so he has no remaining days of subscription and has to pay the full price
            return $price;
        }

        switch ($subscriptionInterval) {
            case SubscriptionInterface::MONTHLY:
                // Our ideal month is ever of 31 days
                $daysInInterval = 31;

                break;
            case SubscriptionInterface::YEARLY:
                // Our ideal year is ever of 365 days
                $daysInInterval = 365;

                break;
            default:
                throw new \InvalidArgumentException(sprintf('The subscription interval can be only "%s" or "%s". "%s" passed.', SubscriptionInterface::MONTHLY, SubscriptionInterface::YEARLY, $subscriptionInterval));
        }

        $pricePerDay = (int) \floor($price->getBaseAmount() / $daysInInterval);

        // Calculate the remaining days
        $nextRenewOn = clone $this->subscription->getNextRenewOn();

        /** @var \DateInterval $remainingDays */
        $remainingDays = $nextRenewOn->diff(new \DateTime());

        $instantPrice = $pricePerDay * $remainingDays->days;

        return new Money([MoneyInterface::BASE_AMOUNT => $instantPrice, MoneyInterface::CURRENCY => $currency]);
    }
}