Further changes, remove Calculators

This commit is contained in:
Patrick Henninger
2022-02-05 23:37:59 +01:00
parent 4415db2be6
commit a4322a2ac3
6 changed files with 132 additions and 198 deletions

View File

@@ -1,37 +0,0 @@
<?php
namespace Gloudemans\Shoppingcart\Calculation;
use Gloudemans\Shoppingcart\CartItem;
use Gloudemans\Shoppingcart\Contracts\Calculator;
use Money\Money;
class DefaultCalculator implements Calculator
{
public static function getAttribute(string $attribute, CartItem $cartItem)
{
switch ($attribute) {
case 'discount':
return $cartItem->price->multiply($cartItem->discountRate, config('cart.rounding', Money::ROUND_UP));
case 'tax':
return $cartItem->priceTarget->multiply($cartItem->taxRate + 1, config('cart.rounding', Money::ROUND_UP));
case 'priceTax':
return $cartItem->priceTarget->add($cartItem->tax);
case 'discountTotal':
return $cartItem->discount->multiply($cartItem->qty, config('cart.rounding', Money::ROUND_UP));
case 'priceTotal':
return $cartItem->price->multiply($cartItem->qty, config('cart.rounding', Money::ROUND_UP));
case 'subtotal':
$subtotal = $cartItem->priceTotal->subtract($cartItem->discountTotal);
return $subtotal->isPositive() ? $subtotal : new Money(0, $cartItem->price->getCurrency());
case 'priceTarget':
return $cartItem->priceTotal->subtract($cartItem->discountTotal)->divide($cartItem->qty);
case 'taxTotal':
return $cartItem->subtotal->multiply($cartItem->taxRate + 1, config('cart.rounding', Money::ROUND_UP));
case 'total':
return $cartItem->subtotal->add($cartItem->taxTotal);
default:
return;
}
}
}

View File

@@ -1,39 +0,0 @@
<?php
namespace Gloudemans\Shoppingcart\Calculation;
use Gloudemans\Shoppingcart\CartItem;
use Gloudemans\Shoppingcart\Contracts\Calculator;
class GrossPrice implements Calculator
{
public static function getAttribute(string $attribute, CartItem $cartItem)
{
$decimals = config('cart.format.decimals', 2);
switch ($attribute) {
case 'priceNet':
return round($cartItem->price / (1 + ($cartItem->taxRate / 100)), $decimals);
case 'discount':
return $cartItem->priceNet * ($cartItem->getDiscountRate() / 100);
case 'tax':
return round($cartItem->priceTarget * ($cartItem->taxRate / 100), $decimals);
case 'priceTax':
return round($cartItem->priceTarget + $cartItem->tax, $decimals);
case 'discountTotal':
return round($cartItem->discount * $cartItem->qty, $decimals);
case 'priceTotal':
return round($cartItem->priceNet * $cartItem->qty, $decimals);
case 'subtotal':
return max(round($cartItem->priceTotal - $cartItem->discountTotal, $decimals), 0);
case 'priceTarget':
return round(($cartItem->priceTotal - $cartItem->discountTotal) / $cartItem->qty, $decimals);
case 'taxTotal':
return round($cartItem->subtotal * ($cartItem->taxRate / 100), $decimals);
case 'total':
return round($cartItem->subtotal + $cartItem->taxTotal, $decimals);
default:
return;
}
}
}

View File

@@ -184,7 +184,7 @@ class Cart
$item->setInstance($this->currentInstance()); $item->setInstance($this->currentInstance());
if (! $keepDiscount) { if (! $keepDiscount) {
$item->setDiscountRate($this->discount); $item->setDiscount($this->discount);
} }
if (!$keepTax) { if (!$keepTax) {
@@ -350,33 +350,19 @@ class Cart
} }
/** /**
* Get the total price of the items in the cart. * Get the discount of the items in the cart.
*
* @return Money
*/ */
public function total(): Money public function price(): Money
{ {
return $this->getContent()->reduce(function (Money $total, CartItem $cartItem) { $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) {
return $total->add($cartItem->total); return $discount->add($cartItem->price());
}, new Money(0, new Currency('USD'))); }, new Money(0, new Currency('USD')));
}
/**
* Get the total tax of the items in the cart.
*/
public function tax(): Money
{
return $this->getContent()->reduce(function (Money $tax, CartItem $cartItem) {
return $tax->add($cartItem->taxTotal);
}, new Money(0, new Currency('USD')));
}
/** if ($calculated instanceof Money) {
* Get the subtotal (total - tax) of the items in the cart. return $calculated;
*/ }
public function subtotal(): Money
{
return $this->getContent()->reduce(function (Money $subTotal, CartItem $cartItem) {
return $subTotal->add($cartItem->subtotal);
}, new Money(0, new Currency('USD')));
} }
/** /**
@@ -386,38 +372,64 @@ class Cart
*/ */
public function discount(): Money public function discount(): Money
{ {
return $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) {
return $discount->add($cartItem->discountTotal); return $discount->add($cartItem->discount());
}, new Money(0, new Currency('USD'))); }, new Money(0, new Currency('USD')));
if ($calculated instanceof Money) {
return $calculated;
}
} }
/** /**
* Get the price of the items in the cart (not rounded). * Get the subtotal (total - tax) of the items in the cart.
*/ */
public function initial(): Money public function subtotal(): Money
{ {
return $this->getContent()->reduce(function (Money $initial, CartItem $cartItem) { $calculated = $this->getContent()->reduce(function (Money $subTotal, CartItem $cartItem) {
return $initial->add($cartItem->price->multiply($cartItem->qty)); return $subTotal->add($cartItem->subtotal());
}, new Money(0, new Currency('USD'))); }, new Money(0, new Currency('USD')));
if ($calculated instanceof Money) {
return $calculated;
}
}
/**
* Get the total tax of the items in the cart.
*/
public function tax(): Money
{
$calculated = $this->getContent()->reduce(function (Money $tax, CartItem $cartItem) {
return $tax->add($cartItem->tax());
}, new Money(0, new Currency('USD')));
if ($calculated instanceof Money) {
return $calculated;
}
} }
/** /**
* Get the price of the items in the cart (previously rounded). * Get the total price of the items in the cart.
*/ */
public function priceTotal(): Money public function total(): Money
{ {
return $this->getContent()->reduce(function (Money $initial, CartItem $cartItem) { $calculated = $this->getContent()->reduce(function (Money $total, CartItem $cartItem) {
return $initial->add($cartItem->priceTotal); return $total->add($cartItem->total());
}, new Money(0, new Currency('USD'))); }, new Money(0, new Currency('USD')));
if ($calculated instanceof Money) {
return $calculated;
}
} }
/** /**
* Get the total weight of the items in the cart. * Get the total weight of the items in the cart.
*/ */
public function weight(): float public function weight(): int
{ {
return $this->getContent()->reduce(function (float $total, CartItem $cartItem) { return $this->getContent()->reduce(function (int $total, CartItem $cartItem) {
return $total + ($cartItem->qty * $cartItem->weight); return $total + $cartItem->weight();
}, 0); }, 0);
} }
@@ -509,7 +521,7 @@ class Cart
{ {
$cartItem = $this->get($rowId); $cartItem = $this->get($rowId);
$cartItem->setDiscountRate($discount); $cartItem->setDiscount($discount);
$content = $this->getContent(); $content = $this->getContent();

View File

@@ -2,30 +2,15 @@
namespace Gloudemans\Shoppingcart; namespace Gloudemans\Shoppingcart;
use Gloudemans\Shoppingcart\Calculation\DefaultCalculator;
use Gloudemans\Shoppingcart\Contracts\Buyable; use Gloudemans\Shoppingcart\Contracts\Buyable;
use Gloudemans\Shoppingcart\Contracts\Calculator;
use Gloudemans\Shoppingcart\Exceptions\InvalidCalculatorException;
use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable; use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Money\Money; use Money\Money;
use Money\Formatter\DecimalMoneyFormatter; use Money\Formatter\DecimalMoneyFormatter;
use Money\Currencies\ISOCurrencies; use Money\Currencies\ISOCurrencies;
use ReflectionClass;
/**
* @property-read mixed discount
* @property-read float discountTotal
* @property-read float priceTarget
* @property-read float priceNet
* @property-read float priceTotal
* @property-read float subtotal
* @property-read float taxTotal
* @property-read float tax
* @property-read float total
* @property-read float priceTax
*/
class CartItem implements Arrayable, Jsonable class CartItem implements Arrayable, Jsonable
{ {
/** /**
@@ -35,10 +20,8 @@ class CartItem implements Arrayable, Jsonable
/** /**
* The ID of the cart item. * The ID of the cart item.
*
* @var int|string
*/ */
public $id; public int|string $id;
/** /**
* The quantity for this cart item. * The quantity for this cart item.
@@ -47,8 +30,6 @@ class CartItem implements Arrayable, Jsonable
/** /**
* The name of the cart item. * The name of the cart item.
*
* @var string
*/ */
public string $name; public string $name;
@@ -74,15 +55,13 @@ class CartItem implements Arrayable, Jsonable
/** /**
* The FQN of the associated model. * The FQN of the associated model.
*
* @var string|null
*/ */
private $associatedModel = null; public ?string $associatedModel = null;
/** /**
* The discount rate for the cart item. * The discount rate for the cart item.
*/ */
public float $discountRate = 0; public float|Money $discount = 0;
/** /**
* The cart instance of the cart item. * The cart instance of the cart item.
@@ -91,10 +70,6 @@ class CartItem implements Arrayable, Jsonable
public function __construct(int|string $id, string $name, Money $price, int $qty = 1, int $weight = 0, ?CartItemOptions $options = null) public function __construct(int|string $id, string $name, Money $price, int $qty = 1, int $weight = 0, ?CartItemOptions $options = null)
{ {
if (!is_string($id) && !is_int($id)) {
throw new \InvalidArgumentException('Please supply a valid identifier.');
}
$this->id = $id; $this->id = $id;
$this->name = $name; $this->name = $name;
$this->price = $price; $this->price = $price;
@@ -142,7 +117,7 @@ class CartItem implements Arrayable, Jsonable
* *
* @param mixed $model * @param mixed $model
*/ */
public function associate($model) : self public function associate(string|Model $model) : self
{ {
$this->associatedModel = is_string($model) ? $model : get_class($model); $this->associatedModel = is_string($model) ? $model : get_class($model);
@@ -162,9 +137,9 @@ class CartItem implements Arrayable, Jsonable
/** /**
* Set the discount rate. * Set the discount rate.
*/ */
public function setDiscountRate(float $discountRate) : self public function setDiscount(float|Money $discount) : self
{ {
$this->discountRate = $discountRate; $this->discount = $discount;
return $this; return $this;
} }
@@ -178,40 +153,70 @@ class CartItem implements Arrayable, Jsonable
return $this; return $this;
} }
/** public function model(): ?Model
* Get an attribute from the cart item or get the associated model.
*
* @return mixed
*/
public function __get(string $attribute)
{ {
if (property_exists($this, $attribute)) { if (isset($this->associatedModel)) {
return $this->{$attribute}; return (new $this->associatedModel())->find($this->id);
}
$decimals = config('cart.format.decimals', 2);
switch ($attribute) {
case 'model':
if (isset($this->associatedModel)) {
return with(new $this->associatedModel())->find($this->id);
}
// no break
case 'modelFQCN':
if (isset($this->associatedModel)) {
return $this->associatedModel;
}
// no break
case 'weightTotal':
return round($this->weight * $this->qty, $decimals);
} }
$class = new ReflectionClass(config('cart.calculator', DefaultCalculator::class)); return null;
if (!$class->implementsInterface(Calculator::class)) { }
throw new InvalidCalculatorException('The configured Calculator seems to be invalid. Calculators have to implement the Calculator Contract.');
}
return call_user_func($class->getName().'::getAttribute', $attribute, $this); /**
* This will is the price of the CartItem considering the set quantity. If you need the raw price
* then simply access the price member.
*/
public function price(): Money
{
return $this->price()->multiply($this->qty);
}
/**
* This is the discount granted for this CartItem. It is based on the given price and, in case
* discount is a float, multiplied or, in case it is an absolute Money, subtracted. It will return
* a minimum value of 0.
*/
public function discount(): Money
{
if ($this->discount instanceof Money) {
return $this->price()->subtract($this->discountRate);
} else {
return $this->price()->multiply($this->discountRate, config('cart.rounding', Money::ROUND_UP));
}
}
/**
* This is the final price of the CartItem but without any tax applied. This does on the
* other hand include any discounts.
*/
public function subtotal(): Money
{
return Money::max(new Money(0, $this->price()->getCurrency()), $this->price()->add($this->discount()));
}
/**
* This is the tax, based on the subtotal (all previous calculations) and set tax rate
*/
public function tax(): Money
{
return $this->subtotal()->multiply($this->taxRate + 1, config('cart.rounding', Money::ROUND_UP));
}
/**
* This is the total price, consisting of the subtotal and tax applied.
*/
public function total(): Money
{
return $this->subtotal()->add($this->tax());
}
/**
* This is the total price, consisting of the subtotal and tax applied.
*/
public function weight(): int
{
return $this->qty * $this->weight;
} }
/** /**
@@ -254,13 +259,16 @@ class CartItem implements Arrayable, Jsonable
'rowId' => $this->rowId, 'rowId' => $this->rowId,
'id' => $this->id, 'id' => $this->id,
'name' => $this->name, 'name' => $this->name,
'qty' => $this->qty,
'price' => self::formatMoney($this->price), 'price' => self::formatMoney($this->price),
'qty' => $this->qty,
'weight' => $this->weight, 'weight' => $this->weight,
'options' => $this->options->toArray(), 'options' => $this->options->toArray(),
'discount' => self::formatMoney($this->discount),
'tax' => self::formatMoney($this->tax), /* Calculated attributes */
'subtotal' => self::formatMoney($this->subtotal), 'discount' => self::formatMoney($this->discount()),
'subtotal' => self::formatMoney($this->subtotal()),
'tax' => self::formatMoney($this->tax()),
'total' => self::formatMoney($this->total()),
]; ];
} }
@@ -293,4 +301,4 @@ class CartItem implements Arrayable, Jsonable
return md5($id . serialize($options)); return md5($id . serialize($options));
} }
} }

View File

@@ -1,10 +0,0 @@
<?php
namespace Gloudemans\Shoppingcart\Contracts;
use Gloudemans\Shoppingcart\CartItem;
interface Calculator
{
public static function getAttribute(string $attribute, CartItem $cartItem);
}

View File

@@ -668,7 +668,7 @@ class CartTest extends TestCase
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(BuyableProduct::class, $cartItem->modelFQCN); $this->assertEquals(BuyableProduct::class, $cartItem->associatedModel);
} }
/** @test */ /** @test */
@@ -682,7 +682,7 @@ class CartTest extends TestCase
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(ProductModel::class, $cartItem->modelFQCN); $this->assertEquals(ProductModel::class, $cartItem->associatedModel);
} }
/** /**
@@ -711,8 +711,8 @@ class CartTest extends TestCase
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertInstanceOf(ProductModel::class, $cartItem->model); $this->assertInstanceOf(ProductModel::class, $cartItem->model());
$this->assertEquals('Some value', $cartItem->model->someValue); $this->assertEquals('Some value', $cartItem->model()->someValue);
} }
/** @test */ /** @test */
@@ -1134,7 +1134,7 @@ class CartTest extends TestCase
'name' => 'First item', 'name' => 'First item',
]), 1); ]), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(50, $cartItem->discountRate); $this->assertEquals(50, $cartItem->discount);
} }
/** @test */ /** @test */
@@ -1158,7 +1158,7 @@ class CartTest extends TestCase
]), 1); ]), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 50); $cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 50);
$this->assertEquals(50, $cartItem->discountRate); $this->assertEquals(50, $cartItem->discount);
} }
/** @test */ /** @test */
@@ -1171,7 +1171,7 @@ class CartTest extends TestCase
]), 2); ]), 2);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(500, $cart->weight()); $this->assertEquals(500, $cart->weight());
$this->assertEquals(500, $cartItem->weightTotal); $this->assertEquals(500, $cartItem->weight());
} }
/** @test */ /** @test */