HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/shop.komma.nl/app/Vat/VatService.php
<?php
namespace App\Vat;


use App\Cart\ShoppingCartItem;
use App\Finance\RoundingService;
use App\Vat\Models\VatScenario;
use Illuminate\Support\Collection;
use Komma\KMS\Core\Attributes\Models\SelectOptionInterface;
use RuntimeException;

/**
 * Class KmsUserService
 *
 * @package App\Users\Kms
 */
class VatService
{
    /** @var Collection */
    private $vatScenarios;

    public static $debug = false;

    /**
     * Returns the current vat rate
     *
     * @return VatScenario
     */
    public function getVatScenario(): VatScenario
    {
        $vatRate = $this->getAllVatScenarios()->where('enumValue', config('shop.vat.default_scenario'))->first();

        /** @var VatScenario $vatRate */
        if(!$vatRate) throw new RuntimeException('Could not get a vat rate. Check shop configuration.');
        return $vatRate;
    }

    /**
     * Adds 3 transient attributes to a model: vat_amount, price_inc and price_ex.
     * Respectively representing the models vat amount, price including vat amount and
     * price excluding vat amount. All in cents.
     *
     * The values are calculated based on the vatScenarioEnum value on the HasFinancialPropertiesInterface implementation.
     * This enum maps to a VatScenario that has a certain percentage and an determines if the HasFinancialPropertiesInterface
     * implementation's vat is including or excluding vat.
     *
     * @see VatScenarioEnum
     * @see VatScenario
     * @param HasFinancialPropertiesInterface $hasFinancialProperties
     *
     * @return HasFinancialPropertiesInterface
     */
    public function calculateVatForModelWithVatScenarioEnum(HasFinancialPropertiesInterface $hasFinancialProperties): HasFinancialPropertiesInterface
    {
        //Check if the hasFinancialProperties implementation also has a vatScenario enum
        if(is_null($hasFinancialProperties->vat_scenario_enum)) throw new \InvalidArgumentException('The passed HasFinancialPropertiesInterface of class '.get_class($hasFinancialProperties).' did not have the required property called "vat_secenario_enum". So vat could not be calculated.');

        //Get the vat scenario based on the hasFinancialProperties implementation's vat_scenario_enum or throw an exception
        /** @var VatScenario $vatScenario */
        $vatScenario = $this->getAllVatScenarios()->where('enumValue', $hasFinancialProperties->vat_scenario_enum)->first();
        if(!$vatScenario) throw new \RuntimeException('The vat_scenario_enum value of '.get_class($hasFinancialProperties).' with id "' . $hasFinancialProperties->id.'" was not a valid one. Make sure it is one of the '.VatScenarioEnum::class.' values');

        //Calculate
        $this->calculateVatForModelUsingVatScenario($hasFinancialProperties, $vatScenario);

        //Make debug info available if needed
        $this->debug('calculated vat information for model with an '.($vatScenario->incVat ? 'inclusive' : 'exclusive').' vat scenario percentage of '.$vatScenario->percentage.'% for class '.get_class($hasFinancialProperties), $hasFinancialProperties);

        return $hasFinancialProperties;
    }

    /**
     * Adds 3 transient attributes to a model: vat_amount, price_inc and price_ex.
     * Respectively representing the models vat amount, price including vat amount and
     * price excluding vat amount. All in cents.
     *
     * The values are calculated based on the given vatScenario value on the HasFinancialPropertiesInterface implementation.
     *
     * @param HasFinancialPropertiesInterface $hasFinancialProperties
     * @param VatScenario                     $vatScenario
     *
     * @return HasFinancialPropertiesInterface
     * @see VatScenario
     * @see VatScenarioEnum
     */
    public function calculateVatForModelUsingVatScenario(HasFinancialPropertiesInterface $hasFinancialProperties, VatScenario $vatScenario): HasFinancialPropertiesInterface {
        if(is_null($hasFinancialProperties->vat_scenario_enum)) throw new \InvalidArgumentException('The passed productable of class '.get_class($hasFinancialProperties).' did not have the required property called "vat_secenario_enum". So vat could not be calculated.');

        switch ($hasFinancialProperties->vat_scenario_enum) {
            case VatScenarioEnum::high_inc:
            case VatScenarioEnum::low_inc:
                $hasFinancialProperties->setVatAmount($this->calculateVatRateAmountFromIncAmount($hasFinancialProperties->getPrice(), $vatScenario));
                $hasFinancialProperties->setPriceInc($hasFinancialProperties->getPrice());
                $hasFinancialProperties->setPriceEx($this->calculateExVatRatePrice($hasFinancialProperties->getPrice(), $vatScenario));
                break;
            case VatScenarioEnum::high_ex:
            case VatScenarioEnum::low_ex:
                $hasFinancialProperties->setVatAmount($this->calculateVatRateAmountFromExAmount($hasFinancialProperties->getPrice(), $vatScenario));
                $hasFinancialProperties->setPriceInc($this->calculateIncVatRatePrice($hasFinancialProperties->getPrice(), $vatScenario));
                $hasFinancialProperties->setPriceEx($hasFinancialProperties->getPrice());
                break;
            case VatScenarioEnum::zero:
                $hasFinancialProperties->setVatAmount(0);
                $hasFinancialProperties->setPriceInc($hasFinancialProperties->getPrice());
                $hasFinancialProperties->setPriceEx($hasFinancialProperties->getPrice());
                break;
        }

        $this->debug('calculated vat information for model with an '.($vatScenario->incVat ? 'inclusive' : 'exclusive').' vat scenario percentage of '.$vatScenario->percentage.'% for class '.get_class($hasFinancialProperties), $hasFinancialProperties);

        return $hasFinancialProperties;
    }

    /**
     * Receives a price in cents and returns the price in cents including vat.
     *
     * @param float            $exPriceInCents
     * @param VatScenario|null $vatScenario If you dont specify it, a default vat rate will be used.
     *
     * @return int The price including vat. The decimals represent fractions of cents.
     */
    public function calculateIncVatRatePrice(float $exPriceInCents, VatScenario $vatScenario = null): int
    {
        if(!$vatScenario) $vatScenario = $this->getVatScenario();
        $priceInc = $exPriceInCents * floatval('1.'.$vatScenario->percentage);
        $result = RoundingService::RoundVat($priceInc);
        $this->debug('Price inc vat ('.$vatScenario->percentage.') from exPrice ('.$exPriceInCents.') = '.$result);

        return $result;
    }

    /**
     * Receives a price in cents, that includes a given vat amount,
     * and returns the price in cents without vat.
     *
     * @param float            $incPriceInCents
     * @param VatScenario|null $vatScenario If you dont specify it, a default vat rate will be used.
     *
     * @return int The price excluding vat.
     */
    public function calculateExVatRatePrice(float $incPriceInCents, VatScenario $vatScenario = null): int
    {
        if(!$vatScenario) $vatScenario = $this->getVatScenario();
        $result = $incPriceInCents / (1 + ($vatScenario->percentage / 100));

        $priceEx = RoundingService::Round($result);
        $this->debug('Price ex vat ('.$vatScenario->percentage.') from incPrice ('.$incPriceInCents.') = '.$priceEx);

        return $priceEx;
    }

    /**
     * @param float $percentage
     * @param float $number
     *
     * @return float
     */
    private function percentageOfNumber(float $percentage, float $number):float {
       return ($percentage * 0.01) * $number;
    }

    /**
     * Calculates the total amount of vat in cents for a certain given amount in cents.
     * Warning! Rounds fractions of cents according to a rule in a config file.
     *
     * @param float            $exPriceInCents
     * @param VatScenario|null $vatScenario If you dont specify it, a default vat rate will be used.
     *
     * @return int
     */
    public function calculateVatRateAmountFromExAmount(float $exPriceInCents, VatScenario $vatScenario = null): int
    {
        if(!$vatScenario) $vatScenario = $this->getVatScenario();
        $vatAmount = $this->percentageOfNumber($vatScenario->percentage, $exPriceInCents);
        $result = RoundingService::RoundVat($vatAmount);
        $this->debug('Price inc vat ('.$vatScenario->percentage.') from exPrice ('.$exPriceInCents.') = '.$result);

        return $result;
    }

    /**
     * Receives a price in cents, that includes a given vat amount, and returns the amount of vat in cents.
     *
     * @param float            $incPriceInCents
     * @param VatScenario|null $vatScenario If you dont specify it, a default vat rate will be used.
     *
     * @return int
     */
    public function calculateVatRateAmountFromIncAmount(float $incPriceInCents, VatScenario $vatScenario = null): int
    {
        if(!$vatScenario) $vatScenario = $this->getVatScenario();
        $priceEx = $this->calculateExVatRatePrice($incPriceInCents, $vatScenario);
        $result = RoundingService::Round($incPriceInCents - $priceEx);
        $this->debug('Vat amount then is: ('.$vatScenario->percentage.') from incPrice ('.$incPriceInCents.') = '.$result);
        return $result;
    }

    /**
     * Return a collection of VatScenario instances, build from the data in the shop config file.
     *
     * @return \Illuminate\Support\Collection
     */
    public function getAllVatScenarios() {
        if(!$this->vatScenarios) {
            $this->vatScenarios = collect(config('shop.vat.scenarios'))->map(function($rateInfo, $enumValue) {
                return $this->getVatScenarioByEnumValue($enumValue);
            });
        }

        return $this->vatScenarios;
    }

    /**
     * Return a collection of VatScenario instances, build from the data in the shop config file.
     *
     * @param mixed (scalar) $enumValue*
     * @return VatScenario
     */
    public function getVatScenarioByEnumValue($enumValue): VatScenario {
        //Retrieve rate information from the config
        $rateInfo = config('shop.vat.scenarios.'.$enumValue);
        if(!$rateInfo) throw new \InvalidArgumentException('Enum value "'.$enumValue.'" not found in config "shop.vat.scenarios"');

        $rateInfo['enumValue'] = $enumValue;
        $vatScenario = new VatScenario();

        //Move the config values into the VatScenario model if their keys exist on the models.
        foreach($rateInfo as $key => $value) if(property_exists($vatScenario, $key)) $vatScenario->$key = $value;

        return $vatScenario;
    }

    /**
     * Return a collection of SelectOptionInterface instances, representing available select options
     *
     * @see SelectOptionInterface
     * @return Collection
     */
    public function getVatScenariosForSelect(): Collection {
        return $this->getAllVatScenarios()->filter(function(VatScenario $vatScenario) {
            return $vatScenario->enabled === true;
        })->map(function(VatScenario $vatScenario) {
            $translation = __('shop/vatrates.scenarios.'.$vatScenario->enumValue.'.price_relation', ['percentage' => config('shop.vat.scenarios.'.$vatScenario->enumValue.'.percentage')]);

            /** @var SelectOptionInterface $selectOption */
            $selectOption = app(SelectOptionInterface::class);
            $selectOption->setValue($vatScenario->enumValue)
                ->setContent($translation)
                ->setHtmlContent($translation);

            return $selectOption;
        });
    }

    /**
     * Logs debug messages to retrospect vat calculations
     *
     * @param array $messages
     */
    private function debug(...$messages) {
        if(!self::$debug) return;

        //Grab scalar variables
        $nonScalars = array_filter($messages, function($data) {
            return !is_scalar($data);
        });

        //Log scalar variables
        \Log::debug(...array_filter($messages, function($data) {
            return is_scalar($data);
        }));

        //Log scalar variables separately, only then Log::debug will log them. If they are a class. We log the FQCN too.
        foreach($nonScalars as $nonScalar) {
            if(is_object($nonScalar)) \Log::debug(get_class($nonScalar).':');
            \Log::debug($nonScalar);
        }
    }
}