diff --git a/src/Calculation/DefaultCalculator.php b/src/Calculation/DefaultCalculator.php deleted file mode 100644 index 22141bb..0000000 --- a/src/Calculation/DefaultCalculator.php +++ /dev/null @@ -1,37 +0,0 @@ -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; - } - } -} diff --git a/src/Calculation/GrossPrice.php b/src/Calculation/GrossPrice.php deleted file mode 100644 index 92cc219..0000000 --- a/src/Calculation/GrossPrice.php +++ /dev/null @@ -1,39 +0,0 @@ -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; - } - } -} diff --git a/src/Cart.php b/src/Cart.php index 04d775a..625a1ad 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -184,7 +184,7 @@ class Cart $item->setInstance($this->currentInstance()); if (! $keepDiscount) { - $item->setDiscountRate($this->discount); + $item->setDiscount($this->discount); } 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) { - return $total->add($cartItem->total); + $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { + return $discount->add($cartItem->price()); }, 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'))); - } - /** - * Get the subtotal (total - tax) of the items in the cart. - */ - public function subtotal(): Money - { - return $this->getContent()->reduce(function (Money $subTotal, CartItem $cartItem) { - return $subTotal->add($cartItem->subtotal); - }, new Money(0, new Currency('USD'))); + if ($calculated instanceof Money) { + return $calculated; + } } /** @@ -386,38 +372,64 @@ class Cart */ public function discount(): Money { - return $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { - return $discount->add($cartItem->discountTotal); + $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { + return $discount->add($cartItem->discount()); }, 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) { - return $initial->add($cartItem->price->multiply($cartItem->qty)); + $calculated = $this->getContent()->reduce(function (Money $subTotal, CartItem $cartItem) { + return $subTotal->add($cartItem->subtotal()); }, 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) { - return $initial->add($cartItem->priceTotal); + $calculated = $this->getContent()->reduce(function (Money $total, CartItem $cartItem) { + return $total->add($cartItem->total()); }, new Money(0, new Currency('USD'))); + + if ($calculated instanceof Money) { + return $calculated; + } } /** * 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 $total + ($cartItem->qty * $cartItem->weight); + return $this->getContent()->reduce(function (int $total, CartItem $cartItem) { + return $total + $cartItem->weight(); }, 0); } @@ -509,7 +521,7 @@ class Cart { $cartItem = $this->get($rowId); - $cartItem->setDiscountRate($discount); + $cartItem->setDiscount($discount); $content = $this->getContent(); diff --git a/src/CartItem.php b/src/CartItem.php index e50a301..38cfecb 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -2,30 +2,15 @@ namespace Gloudemans\Shoppingcart; -use Gloudemans\Shoppingcart\Calculation\DefaultCalculator; use Gloudemans\Shoppingcart\Contracts\Buyable; -use Gloudemans\Shoppingcart\Contracts\Calculator; -use Gloudemans\Shoppingcart\Exceptions\InvalidCalculatorException; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Money\Money; use Money\Formatter\DecimalMoneyFormatter; 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 { /** @@ -35,10 +20,8 @@ class CartItem implements Arrayable, Jsonable /** * The ID of the cart item. - * - * @var int|string */ - public $id; + public int|string $id; /** * The quantity for this cart item. @@ -47,8 +30,6 @@ class CartItem implements Arrayable, Jsonable /** * The name of the cart item. - * - * @var string */ public string $name; @@ -74,15 +55,13 @@ class CartItem implements Arrayable, Jsonable /** * The FQN of the associated model. - * - * @var string|null */ - private $associatedModel = null; + public ?string $associatedModel = null; /** * The discount rate for the cart item. */ - public float $discountRate = 0; + public float|Money $discount = 0; /** * 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) { - if (!is_string($id) && !is_int($id)) { - throw new \InvalidArgumentException('Please supply a valid identifier.'); - } - $this->id = $id; $this->name = $name; $this->price = $price; @@ -142,7 +117,7 @@ class CartItem implements Arrayable, Jsonable * * @param mixed $model */ - public function associate($model) : self + public function associate(string|Model $model) : self { $this->associatedModel = is_string($model) ? $model : get_class($model); @@ -162,9 +137,9 @@ class CartItem implements Arrayable, Jsonable /** * 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; } @@ -178,40 +153,70 @@ class CartItem implements Arrayable, Jsonable return $this; } - - /** - * Get an attribute from the cart item or get the associated model. - * - * @return mixed - */ - public function __get(string $attribute) + + public function model(): ?Model { - if (property_exists($this, $attribute)) { - return $this->{$attribute}; - } - $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); + if (isset($this->associatedModel)) { + return (new $this->associatedModel())->find($this->id); } - $class = new ReflectionClass(config('cart.calculator', DefaultCalculator::class)); - if (!$class->implementsInterface(Calculator::class)) { - throw new InvalidCalculatorException('The configured Calculator seems to be invalid. Calculators have to implement the Calculator Contract.'); - } + return null; + } - 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, 'id' => $this->id, 'name' => $this->name, - 'qty' => $this->qty, 'price' => self::formatMoney($this->price), + 'qty' => $this->qty, 'weight' => $this->weight, 'options' => $this->options->toArray(), - 'discount' => self::formatMoney($this->discount), - 'tax' => self::formatMoney($this->tax), - 'subtotal' => self::formatMoney($this->subtotal), + + /* Calculated attributes */ + '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)); } -} +} \ No newline at end of file diff --git a/src/Contracts/Calculator.php b/src/Contracts/Calculator.php deleted file mode 100644 index 137b1e2..0000000 --- a/src/Contracts/Calculator.php +++ /dev/null @@ -1,10 +0,0 @@ -get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(BuyableProduct::class, $cartItem->modelFQCN); + $this->assertEquals(BuyableProduct::class, $cartItem->associatedModel); } /** @test */ @@ -682,7 +682,7 @@ class CartTest extends TestCase $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'); - $this->assertInstanceOf(ProductModel::class, $cartItem->model); - $this->assertEquals('Some value', $cartItem->model->someValue); + $this->assertInstanceOf(ProductModel::class, $cartItem->model()); + $this->assertEquals('Some value', $cartItem->model()->someValue); } /** @test */ @@ -1134,7 +1134,7 @@ class CartTest extends TestCase 'name' => 'First item', ]), 1); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(50, $cartItem->discountRate); + $this->assertEquals(50, $cartItem->discount); } /** @test */ @@ -1158,7 +1158,7 @@ class CartTest extends TestCase ]), 1); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 50); - $this->assertEquals(50, $cartItem->discountRate); + $this->assertEquals(50, $cartItem->discount); } /** @test */ @@ -1171,7 +1171,7 @@ class CartTest extends TestCase ]), 2); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); $this->assertEquals(500, $cart->weight()); - $this->assertEquals(500, $cartItem->weightTotal); + $this->assertEquals(500, $cartItem->weight()); } /** @test */