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);
}
}
}