mirror of
https://github.com/kevin-DL/LaravelShoppingcart.git
synced 2026-01-11 18:54:33 +00:00
Further changes, remove Calculators
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/Cart.php
88
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();
|
||||
|
||||
|
||||
142
src/CartItem.php
142
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gloudemans\Shoppingcart\Contracts;
|
||||
|
||||
use Gloudemans\Shoppingcart\CartItem;
|
||||
|
||||
interface Calculator
|
||||
{
|
||||
public static function getAttribute(string $attribute, CartItem $cartItem);
|
||||
}
|
||||
@@ -668,7 +668,7 @@ class CartTest extends TestCase
|
||||
|
||||
$cartItem = $cart->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 */
|
||||
|
||||
Reference in New Issue
Block a user