File: D:/HostingSpaces/SBogers10/blije-gasten.komma.pro/app/Komma/Shop/Vat/VatService.php
<?php
namespace App\Komma\Shop\Vat;
use App\Komma\Base\Service;
use App\Komma\Kms\Core\Attributes\Attribute;
use App\Komma\Kms\Core\ModelService;
use App\Komma\Kms\Core\ModelServiceInterface;
use App\Komma\Kms\Core\Sections\SectionTabItem;
use App\Komma\Kms\Core\Tree\NestedSets\Nodes\TreeModel;
use App\Komma\Shop\Categories\Kms\CategorizableInterface;
use App\Komma\Shop\Orders\Models\Order;
use App\Komma\Shop\Products\ProductableInterface;
use App\Komma\Shop\Vat\Models\Rate;
use App\Komma\Sites\HasSitesInterface;
use App\Komma\Sites\Models\Site;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use RuntimeException;
/**
* Class KmsUserService
*
* @package App\Komma\Users\Kms
*/
final class VatService extends ModelService implements ModelServiceInterface
{
protected $sortable = false;
function __construct()
{
$this->forModelName = Rate::class;
parent::__construct();
}
private $vatRate;
/**
* Puts the values of attributes in an Eloquent model. And then saves that model.
*
* @param Model $model
* @param Collection $attributes
* @return Model
*/
public function save(Model $model, Collection $attributes = null): Model
{
/** @var TreeModel|CategorizableInterface $model */
//Process Product and discount Specific attributes
$this->checkContainsAttributes($attributes);
$keys = array_keys($model->getAttributes());
$attributes->each(function(Attribute $attribute) use(&$model, &$keys) {
$valueFrom = $attribute->getsValueFrom();
$valueReference = $attribute->getsValueFromReference();
$value = $attribute->getValue();
switch ($attribute->getsValueFrom()) {
case Attribute::ValueFromItself:
if ($valueReference == 'site_id') {
$this->siteService->linkModelToSitesUsingIdCsvString($model, $value);
}
break;
}
});
$model = parent::saveModel($model, $attributes);
return $model;
}
/**
* Gets the values of an Eloquent model and passes them to a collection of attributes
*
* @param Model $model
* @param Collection $attributes
* @return mixed
*/
public function load(Model $model, Collection $attributes = null): Collection
{
if($attributes == null) return new Collection();
$attributes = $attributes->map(function(Attribute $attribute) use(&$model) {
$valueReference = $attribute->getsValueFromReference();
$valueFrom = $attribute->getsValueFrom();
if ($valueFrom !== Attribute::ValueFromModel) return $attribute;
switch ($valueReference) {
//Case 1. Check if the translation of the model has a value.
case 'site_id':
/** @var HasSitesInterface $model */
$idString = $this->siteService->getSiteIdsForModel($model);
if($idString) $attribute->setValue($idString);
break;
}
return $attribute;
});
return $this->treeService->load($model, $attributes);
}
/**
* Calculates the orders vat amount for the current site and the total order price including vat.
* It fills the vat_amount and total_price (including vat) of an order.
*
* @param $order
*/
public function calculateOrderVatTotal(Order $order) {
if(!$order->total_price_ex || $order->total_price_ex == 0) {
throw new RuntimeException('Could not calculate vat total for order since it does not have a total price excluding vat.');
}
//The total price ex must be an integer or a float with zero decimal
if($order->total_price_ex - floor($order->total_price_ex) != 0) {
throw new RuntimeException('Could not calculate vat total for order since the total price excluding vat isn\'t an integer representing a monetary value in cents.');
}
$order->vat_amount = $this->calculateVatRateAmountFromExAmount($order->total_price_ex);
$order->total_price = $order->total_price_ex + $order->vat_amount; //No rounding needed. Both numbers are integer like.
}
/**
* Returns the vat rate for the current site
*
* @return int
*/
public function getVatRate(): int
{
return config('shop.vat_percentage');
if(isset($this->vatRate)) return $this->vatRate;
$currentSite = $this->siteService->getCurrentSite();
if(!$currentSite->exists) {
$currentSite = Site::first();
}
$vatRates = $this->vatRatesForSite($currentSite)->where('default', '=', true)->get();
if($vatRates->count() > 1) throw new RuntimeException('The site with name "'.$currentSite->name.'" must have exactly 1 vat rate marked as default. But did have '.$vatRates->count().'.');
/** @var Rate $vatRate */
$vatRate = $vatRates->first();
if(!$vatRate) throw new RuntimeException('The site with name "'.$currentSite->name.'" must have vat rate that is marked as default. But did not have one.');
$this->vatRate = $vatRate;
return $vatRate;
}
/**
* Gets the vat rates for a certain site or the current one if your don't specify it.
*
* @param Site|null $site
* @return Builder
*/
public function vatRatesForSite(Site $site = null): Builder
{
if(!$site) {
$site = $this->siteService->getCurrentSite();
if(!$site) throw new RuntimeException('The current site was not set but must be.');
}
$vatRatesQuery = Rate::whereHas('sites', function(Builder $query) use($site) {
$query->where('site_id','=', $site->id);
})->orWhereDoesntHave('sites');
return $vatRatesQuery;
}
/**
* @param ProductableInterface $productable
* @return ProductableInterface
*/
public function calculateVatForProductable(ProductableInterface $productable)
{
$price_ex = $productable->getPrice();
$productable->vat_amount = $this->calculateVatRateAmountFromExAmount($price_ex);
$productable->price_inc = $this->calculateIncVatRatePrice($productable->getPrice());
return $productable;
}
/**
* Receives a price in cents and returns the price in cents including vat.
*
* @param float $exPriceInCents
* @return int The price including vat. The decimals represent fractions of cents.
*/
public function calculateIncVatRatePrice(float $exPriceInCents): int
{
$priceInc = $exPriceInCents * floatval('1.' . $this->getVatRate());
$result = $this->roundVatTotal($priceInc);
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 Rate|null $rate If you dont specify it, a default vat rate will be used.
* @return int The price excluding vat.
*/
public function calculateExVatRatePrice(float $incPriceInCents, Rate $rate = null): int
{
$priceEx = $incPriceInCents / floatval('1.' . $this->getVatRate());
$result = $this->roundVatTotal($priceEx);
return $result;
}
/**
* 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 $amount
* @param Rate|null $rate If you dont specify it, a default vat rate will be used.
* @return int
*/
public function calculateVatRateAmountFromExAmount(float $amount, Rate $rate = null): int
{
$vatAmount = ($this->getVatRate() * 0.01) * $amount;
$result = $this->roundVatTotal($vatAmount);
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 Rate|null $rate If you dont specify it, a default vat rate will be used.
* @return int
*/
public function calculateVatRateAmountFromIncAmount(float $incPriceInCents, Rate $rate = null): int
{
$priceEx = $incPriceInCents / floatval('1.' . $this->getVatRate());
$result = $this->roundVatTotal($incPriceInCents - $priceEx);
return $result;
}
/**
* Rounds the vat total cent fractions to cents.
*
* @param float $vatTotal
* @return int
*/
protected function roundVatTotal(float $vatTotal): int {
$result = round($vatTotal, 0, config('financial.cent_rounding.vat_total', PHP_ROUND_HALF_UP));
return $result;
}
}