From 5bd2614b696a87f94d9c289534511960c6aa17c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 31 Oct 2023 23:32:10 +0300 Subject: [PATCH 01/36] v2 first part --- README.md | 2 +- config/cart.php | 18 +- src/Cart.php | 468 ++------------ src/CartAttribute.php | 16 - src/CartCollection.php | 19 - src/CartItem.php | 168 ++--- src/CartItemCollection.php | 29 + src/CartItemMeta.php | 11 + src/CartItemOption.php | 34 + src/CartServiceProvider.php | 12 +- src/Condition.php | 129 ---- src/ConditionCollection.php | 33 - src/Contracts/CartItemInterface.php | 31 + src/Contracts/Sellable.php | 16 + src/DefaultSessionKey.php | 13 + src/{Storage => Storages}/ArrayStorage.php | 2 +- src/{Storage => Storages}/CacheStorage.php | 2 +- src/{Storage => Storages}/SessionStorage.php | 2 +- .../StorageInterface.php | 2 +- src/Traits/SellableTrait.php | 37 ++ tests/CartTest.php | 597 ++++++++---------- tests/ConditionTest.php | 292 --------- tests/ItemConditionTest.php | 151 ----- tests/ItemTest.php | 21 +- tests/Models/Product.php | 19 + tests/SerializeTest.php | 143 +---- tests/TestCase.php | 14 +- tests/ValidationTest.php | 27 +- 28 files changed, 625 insertions(+), 1683 deletions(-) delete mode 100644 src/CartAttribute.php delete mode 100644 src/CartCollection.php create mode 100644 src/CartItemCollection.php create mode 100644 src/CartItemMeta.php create mode 100644 src/CartItemOption.php delete mode 100644 src/Condition.php delete mode 100644 src/ConditionCollection.php create mode 100644 src/Contracts/CartItemInterface.php create mode 100644 src/Contracts/Sellable.php create mode 100644 src/DefaultSessionKey.php rename src/{Storage => Storages}/ArrayStorage.php (92%) rename src/{Storage => Storages}/CacheStorage.php (92%) rename src/{Storage => Storages}/SessionStorage.php (91%) rename src/{Storage => Storages}/StorageInterface.php (90%) create mode 100644 src/Traits/SellableTrait.php delete mode 100644 tests/ConditionTest.php delete mode 100644 tests/ItemConditionTest.php create mode 100644 tests/Models/Product.php diff --git a/README.md b/README.md index 9e9619e..876087a 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ return [ 'instance' => 'cart', - 'storage' => \Ozdemir\Aurora\Storage\SessionStorage::class, + 'storage' => \Ozdemir\Aurora\Storages\SessionStorage::class, ]; ``` diff --git a/config/cart.php b/config/cart.php index 7934459..dcb45bb 100644 --- a/config/cart.php +++ b/config/cart.php @@ -1,22 +1,20 @@ 'cart', - 'storage' => \Ozdemir\Aurora\Storage\SessionStorage::class, - 'cart_class' => \Ozdemir\Aurora\Cart::class, - 'cart_item' => \Ozdemir\Aurora\CartItem::class, + 'storage' => [ + 'class' => \Ozdemir\Aurora\Storages\SessionStorage::class, - 'cache_store' => config('cache.default'), + 'session_key' => \Ozdemir\Aurora\DefaultSessionKey::class, + ], - 'precision' => 2, + 'cache_store' => env('CART_STORE', config('cache.default')), - 'condition_order' => [ - 'cart' => ['discount', 'other', 'shipping', 'coupon', 'tax'], - 'item' => ['discount', 'other', 'shipping', 'coupon', 'tax'], - ], + 'currency' => [ + 'precision' => 2, + ], ]; diff --git a/src/Cart.php b/src/Cart.php index 77ccc2d..e47e5e4 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -2,489 +2,139 @@ namespace Ozdemir\Aurora; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cookie; -use Ozdemir\Aurora\Storage\StorageInterface; +use Ozdemir\Aurora\Contracts\CartItemInterface; +use Ozdemir\Aurora\Storages\StorageInterface; -class Cart implements \Serializable +class Cart { - /** - * @var CartCollection - */ - protected CartCollection $items; + public CartItemCollection $items; - /** - * @var ConditionCollection - */ - protected ConditionCollection $conditions; + private string $sessionKey ; - /** - * @var array|mixed|string[] - */ - protected array $conditionsOrder; - - /** - * @var array|mixed|string[] - */ - protected array $itemConditionsOrder; - - /** - * @var StorageInterface - */ - private StorageInterface $storage; - - /** - * @var array - */ - private array $meta; - - /** - * @var - */ - private $dispatcher; - - /** - * @var array - */ - private array $config; - - private string $session; - - public function __construct(string $session, StorageInterface $storage, $dispatcher, $config) - { - $this->storage = $storage; - - $this->dispatcher = $dispatcher; - $this->config = array_merge(config('cart'), $config); - - $this->session = $session; - - $this->initCart(); - } - - public function initCart() - { - $this->items = $this->getStorage('cart:items') ?? new CartCollection(); - $this->conditions = $this->getStorage('cart:conditions') ?? new ConditionCollection(); - - $this->conditionsOrder = $this->getStorage('cart:conditionsOrder') ?? config('cart.condition_order.cart'); - $this->itemConditionsOrder = $this->getStorage('cart:itemConditionsOrder') ?? config('cart.condition_order.item'); - $this->meta = $this->getStorage('cart:meta') ?? []; - } - - public function clone(string $session = null, StorageInterface $storage = null, $dispatcher = null, $config = null): Cart - { - $storage ??= $this->storage; - - $session ??= $this->session; - - return new static($session, $storage, $dispatcher ?? $this->dispatcher, $config ?? $this->config); - } - - public function self(): static - { - return $this; - } - - public function getInstanceKey() - { - return $this->storage->instance; - } - - public function load(Cart $oldCart): self + public function __construct(readonly public StorageInterface $storage) { - $this->items = $oldCart->items; - $this->updateItemStorage(); + $storage_session_key = config('cart.storage.session_key'); - $this->conditions = $oldCart->conditions; - $this->updateConditionStorage(); + $this->sessionKey = (new $storage_session_key)(); - return $this; - } - - public function mergeItems(Cart $oldCart): self - { - $this->items = $this->items->merge($oldCart->items); - $this->updateItemStorage(); - - return $this; + $this->items = $this->getStorage('items') ?? new CartItemCollection(); } - public function emit($event, ...$args) + public function make(StorageInterface $storage): static { - $this->dispatcher->dispatch($this->getInstanceKey() . '.' . $event, ...$args); + return new static($storage); } - public function listen($event, $callback) - { - $this->dispatcher->listen($this->getInstanceKey() . '.' . $event, $callback()); - } - - public function add($data) - { - $this->emit('adding'); - - if (is_array(head($data))) { - $added = []; - foreach ($data as $item) { - $added[] = $this->add($item); - } - - return $added; - } - - $cartItemClass = $this->config['cart_item']; - $cartItem = new $cartItemClass($data, $this->itemConditionsOrder, $this->config); - $this->items->updateOrAdd($cartItem); - - $this->updateItemStorage(); - - $this->emit('added'); - - return $this->item($cartItem->hash()); - } - - /** - * @throws \Exception - */ - public function update($key, $data = []) - { - $this->emit('updating'); - - if (is_array($key)) { - foreach ($key as $k => $d) { - $this->update($k, $d); - } - - return; - } - - if (!$this->items->has($key)) { - throw new \Exception('Item has not found..'); - } - - if (is_int($data)) { - if ($data < 1) { - throw new \Exception('Negative or zero value not allowed.'); - } - // increase quantity - $this->items->get($key)->setQuantity($data); - } else { - // update values - $this->items->get($key)->update($data); - } - - $this->updateItemStorage(); - $this->emit('updated'); - } - - /** - * @return CartCollection - */ - public function items(): CartCollection + public function items(): CartItemCollection { return $this->items; } - public function exists($key) - { - return $this->items->has($key); - } - - public function item($key) - { - return $this->items->get($key); - } - - public function remove($key) - { - $this->emit('removing'); - - $this->items->forget($key); - - $this->updateItemStorage(); - $this->emit('removed'); - - return $this; - } - - public function isEmpty() - { - return $this->items->isEmpty(); - } - - public function getItemSubTotalBasePrice() - { - return $this->items->sum(fn (CartItem $item) => $item->getPriceWithoutConditions() * $item->quantity); - } - - public function getItemSubTotal() - { - return round($this->items->sum(fn (CartItem $item) => $item->subtotal()), $this->config['precision']); - } - - public function subtotal() - { - return round($this->calculateConditions($this->getItemSubTotal(), 'subtotal'), $this->config['precision']); - } - - public function total() - { - return round($this->calculateConditions($this->subtotal(), 'total'), $this->config['precision']); - } - - /** - * @param mixed $subtotal - * @return mixed - */ - public function calculateConditions(mixed $subtotal, string $target): mixed + public function add(CartItemInterface ...$cartItems): void { - if ($this->quantity()) { - foreach ($this->conditions(target: $target) as $condition) { - $subtotal += $condition->value; - } + foreach ($cartItems as $cartItem) { + $this->items->updateOrAdd($cartItem); } - - return $subtotal; - } - - public function quantity() - { - return $this->items->sum('quantity'); - } - - public function weight() - { - return $this->items->sum(fn (CartItem $item) => $item->weight() * $item->quantity); - } - - public function clear() - { - $this->emit('clearing'); - $this->items = new CartCollection(); - $this->conditions = new ConditionCollection(); - - $this->updateItemStorage(); - $this->updateConditionStorage(); - $this->emit('cleared'); - - return $this; + $this->updateStorage('items', $this->items); } - public function sync($data) + public function sync(CartItemInterface ...$cartItems): void { $this->clear(); - $this->add($data); - - return $this; - } - - public function condition(Condition $condition) - { - if (!in_array($condition->getTarget(), ['total', 'subtotal'])) { - throw new \Exception("The target for the condition can only be a subtotal or price."); - } - $this->conditions->put($condition->getName(), $condition); - - $this->updateConditionStorage(); - - return $this; - } - - public function setConditions(ConditionCollection $conditions) - { - $this->conditions = $conditions; - - $this->updateConditionStorage(); + $this->add(...$cartItems); + $this->updateStorage('items', $this->items); } - public function getConditions() + public function item(string $hash): CartItemInterface { - return $this->conditions; + // todo: throw NotFound exception + return $this->items->get($hash); } - public function updateConditionPrice() + public function update(string $hash, int $quantity): ?CartItemInterface { - $this->items()->each->calculateConditionPrices(); - - $this->conditions(target: 'subtotal')->calculateSubTotal($this->getItemSubTotal()); - $this->conditions(target: 'total')->calculateSubTotal($this->subtotal()); + return tap($this->item($hash), function(CartItemInterface $cartItem) use ($quantity) { + $cartItem->quantity = $quantity; + $this->updateStorage('items', $this->items); + }); } - public function hasCondition($name) + public function subtotal(): float|int { - return $this->conditions->has($name); + return $this->items->reduce(fn ($total, CartItemInterface $cartItem) => $total + $cartItem->total(), 0); } - public function removeCondition($name) + public function total(): float|int { - $this->conditions->pull($name); - $this->updateConditionStorage(); + return $this->subtotal(); } - public function itemConditions($type = null) + public function weight(): float|int { - return Cart::items() - ->pluck('conditions') - ->flatten(1) - ->groupBy('name') - ->map(function($conditions) { - $condition = $conditions->first(); - $condition->value = $conditions->sum('value'); - - return $condition; - }) - ->pipeInto(ConditionCollection::class) - ->filterType($type); + return $this->items->reduce(fn ($total, CartItemInterface $cartItem) => $total + $cartItem->weight(), 0); } - public function conditions($type = null, $target = null): ConditionCollection + public function remove($hash): void { - return $this->conditions - ->sortByOrder($this->getConditionsOrder()) - ->filterType($type) - ->filterTarget($target); + $this->items->forget($hash); + $this->updateStorage('items', $this->items); } - /** - * @return string[] - */ - public function getConditionsOrder(): array + public function clear(): void { - return $this->conditionsOrder; + $this->items = new CartItemCollection(); + $this->updateStorage('items', $this->items); } - /** - * @return string[] - */ - public function getItemConditionsOrder(): array + public function isEmpty(): bool { - return $this->itemConditionsOrder; - } - - /** - * @param string[] $conditionsOrder - */ - public function setConditionsOrder(array $conditionsOrder): void - { - $this->putStorage('cart:conditionsOrder', $conditionsOrder); - - $this->conditionsOrder = $conditionsOrder; - - $this->updateConditionPrice(); - } - - /** - * @param string[] $itemConditionsOrder - */ - public function setItemConditionsOrder(array $itemConditionsOrder): void - { - $this->putStorage('cart:itemConditionsOrder', $itemConditionsOrder); - - $this->itemConditionsOrder = $itemConditionsOrder; - - $this->items()->each->setItemConditionsOrder($itemConditionsOrder); - } - - /** - * @param string[] $itemConditionsOrder - */ - public function setMeta(string $key, mixed $data): void - { - $this->meta[$key] = $data; - - $this->putStorage('cart:meta', $this->meta); - } - - public function getMeta(string $key, mixed $default): mixed - { - return $this->meta[$key] ?? $default; - } - - public function updateItemStorage() - { - $this->updateConditionPrice(); - - $this->putStorage('cart:items', $this->items); + return $this->items->isEmpty(); } - public function updateConditionStorage() + public function getInstanceKey(): string { - $this->updateConditionPrice(); - - $this->putStorage('cart:conditions', $this->conditions); + return $this->storage->instance; } - public static function defaultSessionKey(): string + public function quantity(): int { - if (Auth::check()) { - return 'user:' . Auth::id(); - } - $guestToken = Cookie::get('guest_token'); - - if (!$guestToken) { - $guestToken = uniqid(); - Cookie::queue('guest_token', $guestToken, 1440); - } - - return 'guest:' . $guestToken; + return $this->items->sum('quantity'); } - public function loadSession($key) + public function loadSession($sessionKey) { - $this->session = $key; + $this->sessionKey = $sessionKey; - $this->initCart(); + $this->items = $this->getStorage('items') ?? new CartItemCollection(); } public function getSessionKey(): string { - return $this->session; + return $this->sessionKey; } - protected function getStorage(string $key) - { - return $this->storage->get($this->getSessionKey() . '.' . $key); - } - - protected function putStorage(string $key, mixed $data) + protected function updateStorage(string $key, mixed $data): void { $this->storage->put($this->getSessionKey() . '.' . $key, $data); } - public function serialize() + protected function getStorage(string $key): mixed { - return serialize($this->__serialize()); + return $this->storage->get($this->getSessionKey() . '.' . $key); } - public function unserialize($string) + public function snapshot(): string { - - $data = unserialize($string, [CartCollection::class, ConditionCollection::class, StorageInterface::class]); - - $this->__unserialize($data); + return serialize($this); } - public function __serialize(): array + public function restore(string $string): static { - return [ - $this->items, - $this->conditions, - $this->conditionsOrder, - $this->itemConditionsOrder, - $this->storage, - ]; - } + $cart = unserialize($string); - public function __unserialize(array $data): void - { - [ - $this->items, - $this->conditions, - $this->conditionsOrder, - $this->itemConditionsOrder, - $this->storage, - ] = $data; + $this->items = $cart->items(); + + return $this; } } diff --git a/src/CartAttribute.php b/src/CartAttribute.php deleted file mode 100644 index 9867bf4..0000000 --- a/src/CartAttribute.php +++ /dev/null @@ -1,16 +0,0 @@ -has([$cartItem->hash])) { - // if the item is already exists increase quantity.. - $this->get($cartItem->hash)->increaseQuantity($cartItem->quantity); - } else { - // otherwise add a new item - $this->put($cartItem->hash, $cartItem); - } - } -} diff --git a/src/CartItem.php b/src/CartItem.php index 7a14bd4..f1dafc1 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -2,182 +2,96 @@ namespace Ozdemir\Aurora; -use Exception; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Validator; -use Ozdemir\Aurora\Traits\CollectionArrayAccess; +use Ozdemir\Aurora\Contracts\Sellable; +use Ozdemir\Aurora\Contracts\CartItemInterface; -class CartItem extends Collection +class CartItem implements CartItemInterface { - use CollectionArrayAccess; + public string $hash; - protected array $itemConditionsOrder; - private array $config; + public Collection $options; - /** - * @throws Exception - */ - public function __construct($items, $itemConditionsOrder, $config) - { - $this->validate($items); - - $items['attributes'] = $this->setAttributes($items['attributes'] ?? []); - $items['conditions'] = new ConditionCollection($items['conditions'] ?? []); - - parent::__construct($items); - - $this->config = $config; - - $this->setItemConditionsOrder($itemConditionsOrder); - $this->calculateConditionPrices(); + public Collection $meta; - $this->put('hash', $this->hash()); - } - - /** - * @param array $itemConditionsOrder - */ - public function setItemConditionsOrder(array $itemConditionsOrder): void + public function __construct(public Sellable $model, public int $quantity, public bool $gift = false) { - $this->itemConditionsOrder = $itemConditionsOrder; - $this->calculateConditionPrices(); - } + $this->options = new Collection(); - /** - * @throws Exception - */ - public function validate($items): void - { - $validator = Validator::make($items, [ - 'id' => ['required'], - 'name' => ['required'], - 'price' => ['required', 'numeric', 'min:0'], - 'quantity' => ['required', 'integer', 'min:1'], - 'attributes' => ['sometimes', 'array'], - 'conditions' => ['sometimes'], - 'weight' => ['sometimes', 'numeric', 'min:0'], - 'sku' => ['sometimes'], - ]); - - if ($validator->fails()) { - throw new Exception($validator->errors()->first()); - } + $this->meta = new Collection(); } - public function hash() + public function hash(): string { - $attr = $this->attributes->pluck('value')->join('-'); - $sku = $this->sku; + $attr = $this->options->pluck('value')->join('-'); - return md5($this->id . $attr . $sku); + return $this->hash = md5($this->model->cartItemId() . $attr . $this->gift); } - public function increaseQuantity(int $value) + public function withOption(string $name, mixed $value, float|int $price = 0, bool $percent = false, float|int $weight = 0): static { - $this->quantity += $value; - } + $option = new CartItemOption($name, $value); - public function setQuantity(int $value) - { - $this->quantity = $value; - } + $option->setPrice($price, $percent); - public function decreaseQuantity(int $value) - { - $this->quantity -= $value; - } + $option->setWeight($weight); - public function getPriceWithoutConditions() - { - return round($this->price + array_sum(Arr::pluck($this->attributes(), 'price')), $this->config['precision']); - } + $this->options->push($option); - public function price(): float - { - return round($this->calculateConditions($this->getPriceWithoutConditions(), 'price'), $this->config['precision']); + return $this; } - public function subtotal(): float + public function withMeta(string $name, mixed $value): static { - $subtotal = $this->price() * $this->quantity; + $this->meta->push(new CartItemOption($name, $value)); - $subtotal = $this->calculateConditions($subtotal, 'subtotal'); - - return round($subtotal, $this->config['precision']); + return $this; } - public function condition(Condition $condition) + public function increaseQuantity(int $value): void { - if (!in_array($condition->getTarget(), ['price', 'subtotal'])) { - throw new \Exception("The target for the condition can only be a price or subtotal."); - } - $this->conditions->put($condition->getName(), $condition); - - $this->calculateConditionPrices(); - - app(config('cart.instance'))->updateItemStorage(); - - return $this; + $this->quantity += $value; } - public function conditions($type = null, $target = null) + public function setQuantity(int $value): void { - return $this->conditions - ->sortByOrder($this->getConditionsOrder()) - ->filterType($type) - ->filterTarget($target); + $this->quantity = $value; } - public function weight() + public function decreaseQuantity(int $value): void { - return $this->weight ?? 0; + $this->quantity -= $value; } - public function attributes() + public function weight(): float|int { - return $this->attributes ?? []; + return $this->model->cartItemWeight() * $this->quantity; } - public function update($data) + public function itemPrice(): float|int { - foreach ($data as $key => $value) { - $this->{$key} = $value; - } + return $this->model->cartItemPrice(); } - /** - * @return string[] - */ - public function getConditionsOrder(): array + public function optionPrice() { - return $this->itemConditionsOrder; + + return $this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice())); } - private function setAttributes($attributes) + public function unitPrice() { - return collect($attributes)->map(fn ($item) => new CartAttribute($item)); + return $this->model->cartItemPrice() + $this->optionPrice(); } - /** - * @param float|int $subtotal - * @return float|int - */ - public function calculateConditions(float|int $subtotal, string $target): int|float + public function subtotal(): float|int { - foreach ($this->conditions(target: $target) as $condition) { - $subtotal += $condition->value; - } - - return $subtotal; + return $this->unitPrice() * $this->quantity; } - /** - * @return void - */ - public function calculateConditionPrices(): void + public function total(): float|int { - $this->conditions(target: 'price')->calculateSubTotal($this->getPriceWithoutConditions()); - $this->conditions(target: 'subtotal')->calculateSubTotal($this->price() * $this->quantity); + return $this->subtotal(); } + } diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php new file mode 100644 index 0000000..5ac1178 --- /dev/null +++ b/src/CartItemCollection.php @@ -0,0 +1,29 @@ +has([$cartItem->hash()])) { + // if the item is already exists increase quantity. + $this->get($cartItem->hash())->increaseQuantity($cartItem->quantity); + } else { + // otherwise add a new item. + $this->put($cartItem->hash(), $cartItem); + } + + return $this->get($cartItem->hash()); + } +} diff --git a/src/CartItemMeta.php b/src/CartItemMeta.php new file mode 100644 index 0000000..9f6cae6 --- /dev/null +++ b/src/CartItemMeta.php @@ -0,0 +1,11 @@ +price = $price; + $this->percent = $percent; + } + + public function getPrice(float|int $cartItemPrice): float|int + { + if (!$this->percent) { + return $this->price; + } + + return $cartItemPrice * $this->price / 100; + } + + public function setWeight(float|int $weight): void + { + $this->weight = $weight; + } +} diff --git a/src/CartServiceProvider.php b/src/CartServiceProvider.php index 0866b1f..13f9cb5 100644 --- a/src/CartServiceProvider.php +++ b/src/CartServiceProvider.php @@ -3,6 +3,7 @@ namespace Ozdemir\Aurora; use Illuminate\Support\ServiceProvider; +use Ozdemir\Aurora\Storages\StorageInterface; class CartServiceProvider extends ServiceProvider { @@ -16,17 +17,14 @@ public function register() $defaultInstance = config('cart.instance'); $this->app->singleton($defaultInstance, function($app) use ($defaultInstance) { + $cartClass = config('cart.cart_class'); + $storageClass = config('cart.storage'); + /* @var StorageInterface $storage */ $storage = new $storageClass($defaultInstance); - $dispatcher = $app->get('events'); - - $cartClass = config('cart.cart_class'); - - $session = $cartClass::defaultSessionKey(); - - return new $cartClass($session, $storage, $dispatcher, config('cart') ?? []); + return new $cartClass($storage); }); } } diff --git a/src/Condition.php b/src/Condition.php deleted file mode 100644 index 94db054..0000000 --- a/src/Condition.php +++ /dev/null @@ -1,129 +0,0 @@ - ['required'], - 'type' => ['required', 'string', Rule::in(config('cart.condition_order.cart'))], - 'target' => ['required', 'string', Rule::in('price', 'subtotal', 'total')], - ]); - - if ($validator->fails()) { - throw new \Exception($validator->errors()->first()); - } - - parent::__construct($items); - } - - public function setActions($actions): void - { - $this->actions = $actions; - } - - public function getValue(): float - { - return $this->value ?? 0; - } - - /** - * @return mixed - */ - public function getName(): mixed - { - return $this->name; - } - - /** - * @return mixed - */ - public function getType(): mixed - { - return $this->type; - } - - /** - * @return mixed - */ - public function getTarget(): mixed - { - return $this->target; - } - - public function calculate($subtotal) - { - return $this->calculateActionValue($subtotal, 0); - } - - public function checkActionRule($rule) - { - if ($rule === null || $rule === [] || $rule === '') { - return true; - } - - if (is_callable($rule)) { - return $rule(); - } - - if (is_bool($rule)) { - return $rule; - } - } - - public function getActionValue($subtotal, $actionVal) - { - if ($subtotal <= 0) { - return 0; - } - - if (is_callable($actionVal)) { - return $actionVal(); - } - - $percentage = Str::endsWith($actionVal, '%'); - $negative = Str::startsWith($actionVal, '-'); - - $value = trim($actionVal, " +-%"); - - return $value * ($negative ? -1 : 1) * ($percentage ? $subtotal * 0.01 : 1); - } - - /** - * @param $subtotal - * @param float|int $value - * @return float|int - */ - public function calculateActionValue($subtotal, float|int $value): int|float - { - $actions = is_array(head($this->actions)) ? $this->actions : [$this->actions]; - - foreach ($actions as $action) { - $actionVal = $action['value']; - - $rule = $this->checkActionRule($action['rules'] ?? null); - - if ($rule) { - $value += $this->getActionValue($subtotal, $actionVal); - } - } - - return $value; - } -} diff --git a/src/ConditionCollection.php b/src/ConditionCollection.php deleted file mode 100644 index 3a14e61..0000000 --- a/src/ConditionCollection.php +++ /dev/null @@ -1,33 +0,0 @@ -sortBy(fn ($item) => array_search($item->type, $order, true))->values(); - } - - public function calculateSubTotal($subtotal) - { - return $this->map(function($condition) use (&$subtotal) { - $condition->value = $condition->calculate($subtotal); - $subtotal += $condition->value; - - return $condition; - }); - } - - public function filterType($type) - { - return $this->when($type, fn ($conditions) => $conditions->where('type', $type)); - } - - public function filterTarget($target) - { - return $this->when($target, fn ($conditions) => $conditions->where('target', $target)); - } -} diff --git a/src/Contracts/CartItemInterface.php b/src/Contracts/CartItemInterface.php new file mode 100644 index 0000000..3605a32 --- /dev/null +++ b/src/Contracts/CartItemInterface.php @@ -0,0 +1,31 @@ +id; + } + + public function cartItemPrice() + { + return $this->price; + } + + public function cartItemBasePrice() + { + return $this->basePrice; + } + + public function cartItemQuantity() + { + return $this->quantity; + } + + public function cartItemWeight() + { + return $this->weight; + } + + public function cartItemAvailability(): bool + { + return true; + } +} diff --git a/tests/CartTest.php b/tests/CartTest.php index e580fc4..fc91a2b 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -1,7 +1,7 @@ toBe(false); \Illuminate\Support\Facades\Auth::login($user); - expect(Auth::id())->toBe($user->id); - expect(Auth::check())->toBe(true); - expect(Cart::getInstanceKey())->toBe('cart'); + expect(Auth::id())->toBe($user->id) + ->and(Auth::check())->toBe(true) + ->and(Cart::getInstanceKey())->toBe('cart'); }); it('can add items to Cart', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + + Cart::add( + new CartItem($product, quantity: 1), + ); - expect(Cart::quantity())->toBe(1); - expect(Cart::isEmpty())->toBeFalse(); + expect(Cart::quantity())->toBe(1) + ->and(Cart::isEmpty())->toBeFalse(); }); it('can add multiple items to Cart', function() { - Cart::add([ - [ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - ], - [ - 'id' => 'headphones', - 'name' => 'Headphones', - 'quantity' => 2, - 'price' => 179, - ], - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 4; + $product2->price = 179; + + Cart::add( + new CartItem($product, 1), + new CartItem($product2, 2), + ); - expect(Cart::items())->toHaveCount(2); - expect(Cart::quantity())->toBe(3); + expect(Cart::items())->toHaveCount(2) + ->and(Cart::quantity())->toBe(3); }); it('increase items count if the id is same', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - ]); - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; - expect(Cart::quantity())->toBe(2); -}); + Cart::add( + new CartItem($product, quantity: 1), + ); -it('can add new item instance with the same item id if the item has a stock keeping unit', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'sku' => 'tshirt-blue-small', - ]); + Cart::add( + new CartItem($product, quantity: 1), + ); - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'sku' => 'tshirt-red-large', - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::quantity())->toBe(2); + expect(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2); }); -it('can have attribute and attribute have to be instance of CartAttribute Class', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'attributes' => [ - 'color' => ['label' => 'Red', 'value' => 'red'], - 'size' => ['label' => 'Large', 'value' => 'l'], - ], - ]); +it('can have options and options have to be instance of CartItemOption Class', function() { - expect(Cart::quantity())->toBe(1); - expect(Cart::items()->first()->attributes)->toBeInstanceOf(Collection::class); - expect(Cart::items()->first()->attributes)->toHaveCount(2); - expect(Cart::items()->first()->attributes->first())->toBeInstanceOf(CartAttribute::class); - expect(Cart::items()->first()->attributes->first()->get('value'))->toBe('red'); -}); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; -it('can add new item instance with the same item id if the item has different attributes without sku', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'attributes' => [ - 'color' => ['label' => 'Red', 'value' => 'red'], - 'size' => ['label' => 'Large', 'value' => 'l'], - ], - ]); + Cart::add( + (new CartItem($product, quantity: 1)) + ->withOption('color', 'red') + ->withOption('size', 'large') + ); - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'attributes' => [ - 'color' => ['label' => 'Blue', 'value' => 'blue'], - 'size' => ['label' => 'Small', 'value' => 's'], - ], - ]); - expect(Cart::items())->toHaveCount(2); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(0); + expect(Cart::quantity())->toBe(1) + ->and(Cart::items()->first()->options)->toBeInstanceOf(Collection::class) + ->and(Cart::items()->first()->options)->toHaveCount(2) + ->and(Cart::items()->first()->options->first())->toBeInstanceOf(\Ozdemir\Aurora\CartItemOption::class) + ->and(Cart::items()->first()->options->first()->value)->toBe('red'); }); -it('can sum total cart item prices', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); +it('can add new item instance with the same item id if the item has different options', function() { + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 0; + + Cart::add( + (new CartItem($product, quantity: 1)) + ->withOption('color', 'red') + ->withOption('size', 'large') + ); + + Cart::add( + (new CartItem($product, quantity: 1)) + ->withOption('color', 'blue') + ->withOption('size', 's') + ); + + expect(Cart::items())->toHaveCount(2) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(0); +}); - expect(Cart::items())->toHaveCount(2); - expect(Cart::total())->toBe(530.0); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::weight())->toBe(10); +it('can sum total cart item prices', function() { + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 1; + + Cart::add( + (new CartItem($product, quantity: 2)) + ); + + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 2; + $product2->price = 470; + $product2->weight = 8; + + Cart::add( + (new CartItem($product2, quantity: 1)) + ); + + expect(Cart::items())->toHaveCount(2) + ->and(Cart::total())->toBe(530) + ->and(Cart::subtotal())->toBe(530) + ->and(Cart::weight())->toBe(10); }); -it('can clear cart ', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); +it('can clear the cart', function() { + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 1; - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(2); + Cart::add(new CartItem($product, quantity: 2)); + + expect(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(2); Cart::clear(); - expect(Cart::isEmpty())->toBeTrue(); - expect(Cart::items())->toHaveCount(0); - expect(Cart::total())->toBe(0.0); - expect(Cart::weight())->toBe(0); + expect(Cart::isEmpty())->toBeTrue() + ->and(Cart::items())->toHaveCount(0) + ->and(Cart::total())->toBe(0) + ->and(Cart::weight())->toBe(0); }); it('can remove item from the cart', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 1; - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); + Cart::add(new CartItem($product, 2)); - expect(Cart::items())->toHaveCount(2); - expect(Cart::total())->toBe(530.0); - expect(Cart::quantity())->toBe(3); - expect(Cart::weight())->toBe(10); + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 2; + $product2->price = 470; + $product2->weight = 8; - $item = Cart::items()->last(); - Cart::remove($item->hash()); + Cart::add(new CartItem($product2, 1)); - expect(Cart::items())->toHaveCount(1); - expect(Cart::total())->toBe(60.0); - expect(Cart::subtotal())->toBe(60.0); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(2); -}); + expect(Cart::items())->toHaveCount(2) + ->and(Cart::total())->toBe(530) + ->and(Cart::quantity())->toBe(3) + ->and(Cart::weight())->toBe(10); -it('can sync the items', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); + $cartItem = Cart::items()->last(); - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::total())->toBe(530.0); - expect(Cart::quantity())->toBe(3); - expect(Cart::weight())->toBe(10); - - Cart::sync([ - [ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 2, - 'price' => 630, - 'weight' => 5, - ], [ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 10, - 'price' => 25, - 'weight' => 1, - ], - ]); + Cart::remove($cartItem->hash()); - expect(Cart::items())->toHaveCount(2); - expect(Cart::total())->toBe(1510.0); - expect(Cart::quantity())->toBe(12); - expect(Cart::weight())->toBe(20); + expect(Cart::items())->toHaveCount(1) + ->and(Cart::total())->toBe(60) + ->and(Cart::subtotal())->toBe(60) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(2); }); -it('can update the items', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - $item = Cart::items()->first(); - - Cart::update($item->hash(), [ - 'id' => 'tshirt', - 'name' => 'Updated T-Shirt', - 'quantity' => 5, - 'price' => 35, - 'weight' => 0.5, - ]); - - expect($item->name)->toBe('Updated T-Shirt'); - expect($item->quantity)->toBe(5); - expect($item->weight())->toBe(0.5); - expect($item->price())->toBe(35.0); - - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(5); +it('can sync the items', function() { + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 1; + + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 2; + $product2->price = 470; + $product2->weight = 8; + + Cart::add( + new CartItem($product, 2), + new CartItem($product2, 1) + ); + + expect(Cart::items())->toHaveCount(2) + ->and(Cart::total())->toBe(530) + ->and(Cart::quantity())->toBe(3) + ->and(Cart::weight())->toBe(10); + + $product->weight = 5; + $product->price = 630; + $product2->weight = 1; + $product2->price = 25; + + Cart::sync( + new CartItem($product, 2), + new CartItem($product2, 10) + ); + + expect(Cart::items())->toHaveCount(2) + ->and(Cart::total())->toBe(1510) + ->and(Cart::quantity())->toBe(12) + ->and(Cart::weight())->toBe(20); }); -it('can update only quantity if the second param is integer', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); +it('can update the quantity', function() { + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 1; + $product->price = 30; + $product->weight = 1; - $item = Cart::items()->first(); + Cart::add(new CartItem($product, 2)); - expect($item->quantity)->toBe(2); - expect(Cart::items())->toHaveCount(1); + $item = Cart::items()->first(); - Cart::update($item->hash(), 20); + $updatedCartItem = Cart::update($item->hash(), 5); - expect($item->quantity)->toBe(20); - expect(Cart::subtotal())->toBe(600.0); - expect(Cart::total())->toBe(600.0); - expect(Cart::items())->toHaveCount(1); + expect($item->quantity)->toBe(5) + ->and(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(5) + ->and($updatedCartItem)->toBeInstanceOf(CartItem::class); }); it('can get items from hash', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 3, - 'price' => 30, - 'weight' => 1, - 'attributes' => [ - 'color' => ['label' => 'Blue', 'value' => 'blue'], - 'size' => ['label' => 'Small', 'value' => 's'], - ], - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + $product->weight = 1; + + Cart::add( + (new CartItem($product, 3)) + ->withOption('color', 'blue') + ->withOption('size', 's') + ); $item = Cart::items()->first(); - expect(Cart::item($item->hash))->toBeInstanceOf(\Ozdemir\Aurora\CartItem::class); - expect(Cart::item($item->hash)->id)->toBe('tshirt'); - expect(Cart::item($item->hash)->name)->toBe('T-Shirt'); - expect(Cart::item($item->hash)->quantity)->toBe(3); - expect(Cart::item($item->hash)->price)->toBe(30); - expect(Cart::item($item->hash)->weight)->toBe(1); - expect(Cart::item($item->hash)->attributes)->toBeInstanceOf(Collection::class); - expect(Cart::item($item->hash)->attributes)->toHaveCount(2); - expect(Cart::item($item->hash)->attributes->first())->toBeInstanceOf(CartAttribute::class); - expect(Cart::item($item->hash)->attributes->first()->label)->toBe('Blue'); - expect(Cart::item($item->hash)->attributes->last()->label)->toBe('Small'); + expect(Cart::item($item->hash))->toBeInstanceOf(\Ozdemir\Aurora\CartItem::class) + ->and(Cart::item($item->hash)->model->id)->toBe(3) + ->and(Cart::item($item->hash)->quantity)->toBe(3) + ->and(Cart::item($item->hash)->unitPrice())->toBe(30) + ->and(Cart::item($item->hash)->weight())->toBe(3) + ->and(Cart::item($item->hash)->options)->toBeInstanceOf(Collection::class) + ->and(Cart::item($item->hash)->options)->toHaveCount(2) + ->and(Cart::item($item->hash)->options->first())->toBeInstanceOf(\Ozdemir\Aurora\CartItemOption::class) + ->and(Cart::item($item->hash)->options->first()->label)->toBe('color') + ->and(Cart::item($item->hash)->options->first()->value)->toBe('blue') + ->and(Cart::item($item->hash)->options->last()->label)->toBe('size'); }); it('can initialize a new instance', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 4, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; - expect(Cart::items())->toHaveCount(1); + Cart::add(new CartItem($product, 2)); - $newStorage = new \Ozdemir\Aurora\Storage\ArrayStorage('wishlist'); + expect(Cart::items())->toHaveCount(1); - $wishlist = Cart::clone(Cart::defaultSessionKey(), $newStorage); + $wishlist = Cart::make( new \Ozdemir\Aurora\Storages\ArrayStorage('wishlist')); expect($wishlist->items())->toHaveCount(0); - $wishlist->add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 100, - 'price' => 30, - 'weight' => 5, - ]); - expect($wishlist->items())->toHaveCount(1); - expect($wishlist->quantity())->toBe(100); - expect($wishlist->weight())->toBe(500); - expect($wishlist->getInstanceKey())->toBe('wishlist'); - - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(8); - expect(Cart::getInstanceKey())->toBe('cart'); + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 3; + $product2->price = 20; + $product2->weight = 5; + + $wishlist->add(new CartItem($product2, 100)); + + expect($wishlist->items())->toHaveCount(1) + ->and($wishlist->quantity())->toBe(100) + ->and($wishlist->weight())->toBe(500) + ->and($wishlist->getInstanceKey())->toBe('wishlist') + ->and(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(8) + ->and(Cart::getInstanceKey())->toBe('cart'); }); it('can refresh user id after login', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 4, - ]); - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(8); - expect(Cart::getInstanceKey())->toBe('cart'); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + expect(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(8) + ->and(Cart::getInstanceKey())->toBe('cart'); $user = (new \Illuminate\Foundation\Auth\User())->forceFill([ 'id' => 123, @@ -405,32 +335,31 @@ expect(Auth::id())->toBe($user->id); expect(Auth::check())->toBe(true); - Cart::loadSession(Auth::id()); + Cart::loadSession('user:'. Auth::id()); - expect(Cart::getSessionKey())->toBe('123'); + expect(Cart::getSessionKey())->toBe('user:'. Auth::id()) + ->and(Cart::items())->toHaveCount(0) + ->and(Cart::quantity())->toBe(0) + ->and(Cart::weight())->toBe(0) + ->and(Cart::total())->toBe(0) + ->and(Cart::getInstanceKey())->toBe('cart'); - expect(Cart::items())->toHaveCount(0); - expect(Cart::quantity())->toBe(0); - expect(Cart::weight())->toBe(0); - expect(Cart::total())->toBe(0.0); - expect(Cart::getInstanceKey())->toBe('cart'); }); it('can load any session Cart', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 4, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(8); - expect(Cart::getInstanceKey())->toBe('cart'); - expect(Cart::getSessionKey())->toContain('guest:'); + Cart::add(new CartItem($product, 2)); + + expect(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(8) + ->and(Cart::getInstanceKey())->toBe('cart') + ->and(Cart::getSessionKey())->toContain('guest:'); $user = (new \Illuminate\Foundation\Auth\User())->forceFill([ 'id' => 123, @@ -446,21 +375,19 @@ expect(Auth::id())->toBe($user->id); expect(Auth::check())->toBe(true); - Cart::loadSession(Auth::id()); + Cart::loadSession('user:'. Auth::id()); - expect(Cart::getSessionKey())->toBe('123'); - - expect(Cart::items())->toHaveCount(0); - expect(Cart::quantity())->toBe(0); - expect(Cart::weight())->toBe(0); - expect(Cart::total())->toBe(0.0); - expect(Cart::getInstanceKey())->toBe('cart'); + expect(Cart::getSessionKey())->toBe('user:'. Auth::id()) + ->and(Cart::items())->toHaveCount(0) + ->and(Cart::quantity())->toBe(0) + ->and(Cart::weight())->toBe(0) + ->and(Cart::total())->toBe(0) + ->and(Cart::getInstanceKey())->toBe('cart'); Cart::loadSession($oldSession); - expect(Cart::items())->toHaveCount(1); - expect(Cart::quantity())->toBe(2); - expect(Cart::weight())->toBe(8); - expect(Cart::getInstanceKey())->toBe('cart'); - + expect(Cart::items())->toHaveCount(1) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::weight())->toBe(8) + ->and(Cart::getInstanceKey())->toBe('cart'); }); diff --git a/tests/ConditionTest.php b/tests/ConditionTest.php deleted file mode 100644 index 27ca846..0000000 --- a/tests/ConditionTest.php +++ /dev/null @@ -1,292 +0,0 @@ - 'VAT (6%)', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - - $condition->setActions([ - [ - 'value' => '6%', - 'rules' => [], - ], - ]); - - expect(Cart::hasCondition('VAT (6%)'))->toBeFalse(); - - Cart::condition($condition); - - expect(Cart::hasCondition('VAT (6%)'))->toBeTrue(); - - expect(Cart::conditions())->toBeInstanceOf(\Ozdemir\Aurora\ConditionCollection::class); - expect(Cart::conditions())->toHaveCount(1); - - expect(Cart::conditions()->first())->toBeInstanceOf(\Ozdemir\Aurora\Condition::class); - expect(Cart::conditions()->first()->name)->toBe('VAT (6%)'); -}); - -it('can remove a condition', function() { - $condition = new Ozdemir\Aurora\Condition([ - 'name' => 'VAT (6%)', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - - $condition->setActions([ - [ - 'value' => '6%', - 'rules' => [], - ], - ]); - - Cart::condition($condition); - - expect(Cart::hasCondition('VAT (6%)'))->toBeTrue(); - - Cart::removeCondition('VAT (6%)'); - - expect(Cart::hasCondition('VAT (6%)'))->toBeFalse(); - - expect(Cart::conditions())->toBeInstanceOf(\Ozdemir\Aurora\ConditionCollection::class); - expect(Cart::conditions())->toHaveCount(0); -}); - -it('can add value to total', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(530.0); - - $condition = new Ozdemir\Aurora\Condition([ - 'name' => 'Shipping', - 'type' => 'shipping', - 'target' => 'total', - ]); - - $condition->setActions([ - [ - 'value' => '25', - ], - ]); - - Cart::condition($condition); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(555.0); -}); - -it('can sub value from total ', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(530.0); - - $condition = new Ozdemir\Aurora\Condition([ - 'name' => 'Discount', - 'type' => 'discount', - 'target' => 'total', - ]); - - $condition->setActions([ - [ - 'value' => '-25', - ], - ]); - - Cart::condition($condition); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(505.0); -}); - -it('can add percent value to total ', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(530.0); - - $condition = new Ozdemir\Aurora\Condition([ - 'name' => 'Tax (6%)', - 'type' => 'tax', - 'target' => 'total', - ]); - - $condition->setActions([ - [ - 'value' => '6%', - ], - ]); - - Cart::condition($condition); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(561.8); -}); - -it('can sub percent value from total ', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 30, - 'weight' => 1, - ]); - - Cart::add([ - 'id' => 'tv', - 'name' => 'Television', - 'quantity' => 1, - 'price' => 470, - 'weight' => 8, - ]); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(530.0); - - $condition = new Ozdemir\Aurora\Condition([ - 'name' => 'Tax (6%)', - 'type' => 'tax', - 'target' => 'total', - ]); - - $condition->setActions([ - [ - 'value' => '-6%', - ], - ]); - - Cart::condition($condition); - - expect(Cart::items())->toHaveCount(2); - expect(Cart::subtotal())->toBe(530.0); - expect(Cart::total())->toBe(498.2); -}); - -it('can set condition orders', function() { - $shipping = new Ozdemir\Aurora\Condition([ - 'name' => 'Shipping', - 'type' => 'shipping', - 'target' => 'subtotal', - ]); - $shipping->setActions([['value' => '25']]); - Cart::condition($shipping); - - $tax = new Ozdemir\Aurora\Condition([ - 'name' => 'Tax', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - $tax->setActions([['value' => '6%']]); - Cart::condition($tax); - - $other = new Ozdemir\Aurora\Condition([ - 'name' => 'Other', - 'type' => 'other', - 'target' => 'subtotal', - ]); - $other->setActions([['value' => '125']]); - Cart::condition($other); - - $coupon = new Ozdemir\Aurora\Condition([ - 'name' => 'Coupon', - 'type' => 'coupon', - 'target' => 'total', - ]); - $coupon->setActions([['value' => '-5']]); - Cart::condition($coupon); - - $discount = new Ozdemir\Aurora\Condition([ - 'name' => 'Discount', - 'type' => 'discount', - 'target' => 'subtotal', - ]); - $discount->setActions([['value' => '-10%']]); - Cart::condition($discount); - - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 100, - 'weight' => 1, - ]); - - expect(Cart::getConditionsOrder())->toBe(['discount', 'other', 'shipping', 'coupon', 'tax']); - expect(Cart::conditions()->first()->name)->toBe('Discount'); - expect(Cart::conditions()->get(1)->name)->toBe('Other'); - expect(Cart::conditions()->get(2)->name)->toBe('Shipping'); - expect(Cart::conditions()->get(3)->name)->toBe('Coupon'); - expect(Cart::conditions()->last()->name)->toBe('Tax'); - - // (((200*0,9)+125)+25)*1,06 = 349.8 - expect(Cart::subtotal())->toBe(349.8); - // 349.8 - 5.0 = 344.8 - expect(Cart::total())->toBe(344.8); - - Cart::setConditionsOrder(['other', 'shipping', 'tax', 'coupon', 'discount']); - - expect(Cart::getConditionsOrder())->toBe(['other', 'shipping', 'tax', 'coupon', 'discount']); - expect(Cart::conditions()->first()->name)->toBe('Other'); - expect(Cart::conditions()->get(1)->name)->toBe('Shipping'); - expect(Cart::conditions()->get(2)->name)->toBe('Tax'); - expect(Cart::conditions()->get(3)->name)->toBe('Coupon'); - expect(Cart::conditions()->last()->name)->toBe('Discount'); - - // (200+125+25)*1,06*0,9 = 333.9 - expect(Cart::subtotal())->toBe(333.9); - // 333.9 - 5.0 = 328.9 - expect(Cart::total())->toBe(328.9); -}); diff --git a/tests/ItemConditionTest.php b/tests/ItemConditionTest.php deleted file mode 100644 index 95ac020..0000000 --- a/tests/ItemConditionTest.php +++ /dev/null @@ -1,151 +0,0 @@ -toBeTrue(); - - $item = Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 100, - ]); - - $option = new Ozdemir\Aurora\Condition([ - 'name' => 'Option', - 'type' => 'other', - 'target' => 'price', - ]); - - $option->setActions([['value' => '10%']]); - - $item->condition($option); - - expect($item->price())->toBe(110.0); - expect($item->subtotal())->toBe(220.0); - - - $item2 = Cart::add([ - 'id' => 'tshirt 2', - 'name' => 'T-Shirt 2', - 'quantity' => 2, - 'price' => 100, - ]); - - $option2 = new Ozdemir\Aurora\Condition([ - 'name' => 'Option 2', - 'type' => 'other', - 'target' => 'subtotal', - ]); - - $option2->setActions([['value' => '10%']]); - $item2->condition($option2); - - expect($item2->price())->toBe(100.0); - expect($item2->subtotal())->toBe(220.0); - - expect(Cart::quantity())->toBe(4); - expect(Cart::isEmpty())->toBeFalse(); -}); - - -it('can set items condition orders', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 100, - 'weight' => 1, - ]); - - $tax = new Ozdemir\Aurora\Condition([ - 'name' => 'Tax', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - $tax->setActions([['value' => '6%']]); - - $other = new Ozdemir\Aurora\Condition([ - 'name' => 'Other', - 'type' => 'other', - 'target' => 'subtotal', - ]); - $other->setActions([['value' => '125']]); - - expect(Cart::items())->toHaveCount(1); - - expect(Cart::total())->toBe(200.0); - expect(Cart::getItemConditionsOrder())->toBe(['discount', 'other', 'shipping', 'coupon', 'tax']); - - $item = Cart::items()->first(); - $item->condition($tax); - $item->condition($other); - // (2*100 + 125) * 1,06 - expect(Cart::total())->toBe(344.5); - - Cart::setItemConditionsOrder(['discount', 'shipping', 'tax', 'other', 'coupon']); - - // (2*100 * 1,06) + 125 - expect(Cart::getItemConditionsOrder())->toBe(['discount', 'shipping', 'tax', 'other', 'coupon']); - - expect(Cart::total())->toBe(337.0); -}); - -it('can set items condition orders and update existing items', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 100, - 'weight' => 1, - ]); - - $tax = new Ozdemir\Aurora\Condition([ - 'name' => 'Tax', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - $tax->setActions([['value' => '6%']]); - - $other = new Ozdemir\Aurora\Condition([ - 'name' => 'Other', - 'type' => 'other', - 'target' => 'subtotal', - ]); - $other->setActions([['value' => '125']]); - - expect(Cart::items())->toHaveCount(1); - - expect(Cart::total())->toBe(200.0); - expect(Cart::getItemConditionsOrder())->toBe(['discount', 'other', 'shipping', 'coupon', 'tax']); - - $item = Cart::items()->first(); - $item->condition($tax); - $item->condition($other); - // (2*100 + 125) * 1,06 - expect(Cart::total())->toBe(344.5); - - // dont update existing items - Cart::setItemConditionsOrder(['discount', 'coupon', 'tax', 'shipping', 'other'], false); - - expect(Cart::getItemConditionsOrder())->toBe(['discount', 'coupon', 'tax', 'shipping', 'other']); - - // (2*100 * 1,06) + 125 - expect(Cart::total())->toBe(337.0); - - Cart::add([ - 'id' => 'tshirt-new', - 'name' => 'T-Shirt New', - 'quantity' => 2, - 'price' => 100, - 'weight' => 1, - ]); - - $item = Cart::items()->last(); - $item->condition($tax); - $item->condition($other); - - // 337 + 337 - expect(Cart::total())->toBe(674.0); -}); diff --git a/tests/ItemTest.php b/tests/ItemTest.php index da6e8f6..96f9404 100644 --- a/tests/ItemTest.php +++ b/tests/ItemTest.php @@ -1,20 +1,23 @@ toBeTrue(); - $item = Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 2, - 'price' => 100, - ]); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 100; - expect($item)->toBeInstanceOf(config('cart.cart_item')); + Cart::add( + $item = new CartItem($product, quantity: 2), + ); + + expect(get_class($item))->toImplement(\Ozdemir\Aurora\Contracts\CartItemInterface::class) + ->and(Cart::quantity())->toBe(2) + ->and(Cart::isEmpty())->toBeFalse(); - expect(Cart::quantity())->toBe(2); - expect(Cart::isEmpty())->toBeFalse(); }); diff --git a/tests/Models/Product.php b/tests/Models/Product.php new file mode 100644 index 0000000..46c0bf6 --- /dev/null +++ b/tests/Models/Product.php @@ -0,0 +1,19 @@ + 'Condition', - 'type' => 'tax', - 'target' => 'subtotal', - ]); - $condition->setActions([ - [ - 'value' => '125', - 'rules' => new SerializableClosure(fn () => Cart::subtotal() > 50), - ], - ]); - Cart::condition($condition); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + $product->weight = 1; - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 3, - 'price' => 30, - 'weight' => 1, - 'attributes' => [ - 'color' => ['label' => 'Blue', 'value' => 'blue'], - 'size' => ['label' => 'Small', 'value' => 's'], - ], - ]); - expect(Cart::total())->toBe(215.0); - expect(Cart::items())->toHaveCount(1); - expect(Cart::serialize())->toBeString(); + Cart::add( + (new CartItem($product, 1)) + ->withOption('color', 'blue') + ->withOption('size', 's') + ); + expect(Cart::snapshot())->toBeString(); }); it('can unserialize from serialized string', function() { expect(Cart::isEmpty())->toBeTrue(); - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 3, - 'price' => 30, - 'weight' => 1, - 'attributes' => [ - 'color' => ['label' => 'Blue', 'value' => 'blue'], - 'size' => ['label' => 'Small', 'value' => 's'], - ], - ]); - - $condition = new Condition([ - 'name' => 'Condition A', - 'type' => 'coupon', - 'target' => 'subtotal', - ]); - - $condition->setActions([ - [ - 'value' => '-50%', - 'rules' => new SerializableClosure(function() { - // we shouldn't check Cart::subtotal() here - // since we apply the discount on subtotal. - // Instead, we check product subtotal. - return Cart::getItemSubTotal() > 100; - }), - ], - ]); - - Cart::condition($condition); + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 40; + $product->weight = 1; + + Cart::add( + (new CartItem($product, 3)) // 40 * 3 = 120 + ->withOption('color', 'blue') + ->withOption('size', 's', '10') // + 10 + ->withOption('size', 's', '5', true) // + 40 * 0,05 = + 2 // total 40 + 10 + 2 (56 * 3 = 156) + ); // Get serialized text.. - $serialized = Cart::serialize(); + $snapshot = Cart::snapshot(); // Clear Cart::clear(); + // It is now empty expect(Cart::isEmpty())->toBeTrue(); - // init the cart from serialized string - Cart::unserialize($serialized); - - expect(Cart::subtotal())->toBe(90.0); - expect(Cart::total())->toBe(90.0); - expect(Cart::items()->count())->toBe(1); - expect(Cart::quantity())->toBe(3); - - // add more item to be able to pass the condition rule - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'weight' => 1, - 'attributes' => [ - 'color' => ['label' => 'Blue', 'value' => 'blue'], - 'size' => ['label' => 'Small', 'value' => 's'], - ], - ]); - - - expect(Cart::items()->count())->toBe(1); - expect(Cart::quantity())->toBe(4); - // The quantity is not 4 - // And the subtotal is 120 ( 120 > 100 ). Rule returns true - // It applies the -50% on subtotal - // It applies the -50% on subtotal - expect(Cart::subtotal())->toBe(60.0); - expect(Cart::total())->toBe(60.0); - - - // if we create a new condition on targets total value - $condition2 = new Condition([ - 'name' => 'Condition B', - 'type' => 'shipping', - 'target' => 'total', - ]); - - $condition2->setActions([ - [ - 'value' => '20', - 'rules' => new SerializableClosure(function() { - // we can check an item quantity here.. - return Cart::quantity() < 5; - }), - ], - ]); - Cart::condition($condition2); + // restore the cart from snapshot + Cart::restore($snapshot); - expect(Cart::subtotal())->toBe(60.0); - expect(Cart::total())->toBe(80.0); + expect(Cart::subtotal())->toBe(156) + ->and(Cart::total())->toBe(156) + ->and(Cart::items()->count())->toBe(1) + ->and(Cart::quantity())->toBe(3); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 944ff01..a75c3ac 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,9 +4,8 @@ use Illuminate\Events\Dispatcher; use Orchestra\Testbench\TestCase as Orchestra; -use Ozdemir\Aurora\Cart; use Ozdemir\Aurora\CartServiceProvider; -use Ozdemir\Aurora\Storage\ArrayStorage; +use Ozdemir\Aurora\Storages\ArrayStorage; class TestCase extends Orchestra { @@ -14,12 +13,15 @@ protected function setUp(): void { parent::setUp(); - $this->app->singleton('laravel-cart', function($app) { - $storage = new ArrayStorage('cart'); + $this->app->singleton('cart', function($app) { + $cartClass = config('cart.cart_class'); - $session = Cart::defaultSessionKey(); + // $storageClass = config('cart.storage'); + // /* @var StorageInterface $storage */ + // $storage = new $storageClass('cart'); + $storage = new ArrayStorage('cart'); - return new Cart($session, $storage, new Dispatcher(), config('cart') ?? []); + return new $cartClass($storage); }); } diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index eadd129..1470bd5 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -3,28 +3,9 @@ use Ozdemir\Aurora\Facades\Cart; it('can not update if the quantity is negative', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => 1, - 'price' => 30, - 'weight' => 1, - ]); - - $item = Cart::items()->first(); - - expect($item->quantity)->toBe(1); - expect(Cart::items())->toHaveCount(1); - - Cart::update($item->hash(), 0); -})->throws(Exception::class); + // NegativeQuantityException +})->throws(Exception::class)->todo(); it('can throw validation error when the data is not valid', function() { - Cart::add([ - 'id' => 'tshirt', - 'name' => 'T-Shirt', - 'quantity' => -5, - 'price' => 30, - 'weight' => 1, - ]); -})->throws(Exception::class); + // InvalidDataException +})->throws(Exception::class)->todo(); From c141ab02feacefbba1cc74fd5e8cd103fc633f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Wed, 1 Nov 2023 00:12:45 +0300 Subject: [PATCH 02/36] v2 second part, add Money class --- config/cart.php | 1 + src/Cart.php | 10 +++--- src/CartItem.php | 20 ++++++------ src/CartItemCollection.php | 8 +++++ src/Facades/Cart.php | 22 ++++++-------- src/Money.php | 62 ++++++++++++++++++++++++++++++++++++++ tests/CartTest.php | 22 +++++++------- tests/SerializeTest.php | 4 +-- 8 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 src/Money.php diff --git a/config/cart.php b/config/cart.php index dcb45bb..3c3a59d 100644 --- a/config/cart.php +++ b/config/cart.php @@ -14,6 +14,7 @@ 'cache_store' => env('CART_STORE', config('cache.default')), 'currency' => [ + 'class' => \Ozdemir\Aurora\Money::class, 'precision' => 2, ], diff --git a/src/Cart.php b/src/Cart.php index e47e5e4..c98c335 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -9,7 +9,7 @@ class Cart { public CartItemCollection $items; - private string $sessionKey ; + private string $sessionKey; public function __construct(readonly public StorageInterface $storage) { @@ -60,14 +60,14 @@ public function update(string $hash, int $quantity): ?CartItemInterface }); } - public function subtotal(): float|int + public function subtotal(): Money { - return $this->items->reduce(fn ($total, CartItemInterface $cartItem) => $total + $cartItem->total(), 0); + return $this->items->subtotal(); } - public function total(): float|int + public function total(): Money { - return $this->subtotal(); + return $this->subtotal()->round(); } public function weight(): float|int diff --git a/src/CartItem.php b/src/CartItem.php index f1dafc1..69adef0 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -68,30 +68,28 @@ public function weight(): float|int return $this->model->cartItemWeight() * $this->quantity; } - public function itemPrice(): float|int + public function itemPrice(): Money { - return $this->model->cartItemPrice(); + return new Money($this->model->cartItemPrice()); } - public function optionPrice() + public function optionPrice(): Money { - - return $this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice())); + return new Money($this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice()->amount()))); } - public function unitPrice() + public function unitPrice(): Money { - return $this->model->cartItemPrice() + $this->optionPrice(); + return $this->itemPrice()->add($this->optionPrice())->round(); } - public function subtotal(): float|int + public function subtotal(): Money { - return $this->unitPrice() * $this->quantity; + return $this->unitPrice()->multiply($this->quantity); } - public function total(): float|int + public function total(): Money { return $this->subtotal(); } - } diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php index 5ac1178..917aefa 100644 --- a/src/CartItemCollection.php +++ b/src/CartItemCollection.php @@ -3,6 +3,7 @@ namespace Ozdemir\Aurora; use Illuminate\Support\Collection; +use Ozdemir\Aurora\Contracts\CartItemInterface; class CartItemCollection extends Collection { @@ -26,4 +27,11 @@ public function updateOrAdd(CartItem $cartItem): CartItem return $this->get($cartItem->hash()); } + + public function subtotal(): Money + { + return new Money( + $this->sum(fn (CartItemInterface $cartItem) => $cartItem->total()->amount()) + ); + } } diff --git a/src/Facades/Cart.php b/src/Facades/Cart.php index b0fcdca..7749bc7 100644 --- a/src/Facades/Cart.php +++ b/src/Facades/Cart.php @@ -5,22 +5,18 @@ use Illuminate\Support\Facades\Facade; /** - * @see \Ozdemir\Aurora\Cart + * Class Cart + * + * Facade for the Cart class in the Ozdemir\Aurora namespace. + * + * @package Ozdemir\Aurora + * + * @mixin \Ozdemir\Aurora\Cart + * */ class Cart extends Facade { - /** - * Class Cart - * - * Facade for the Cart class in the Ozdemir\Aurora namespace. - * - * @package Ozdemir\Aurora - * - * @mixin \Ozdemir\Aurora\Cart - * @see \Ozdemir\Aurora\Cart - * - */ - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { return 'cart'; } diff --git a/src/Money.php b/src/Money.php new file mode 100644 index 0000000..89dd4f2 --- /dev/null +++ b/src/Money.php @@ -0,0 +1,62 @@ +amount; + } + + public function round($precision = null, $mode = PHP_ROUND_HALF_UP): static + { + if (is_null($precision)) { + $precision = 2; +// $precision = config('cart.precision'); + } + + $amount = round($this->amount, $precision, $mode); + + return $this->newInstance($amount); + } + + public function add(self $addend): static + { + return $this->newInstance($this->amount + $addend->amount); + } + + public function subtract(self $subtrahend): static + { + return $this->newInstance($this->amount - $subtrahend->amount); + } + + public function multiply($multiplier): static + { + return $this->newInstance($this->amount * $multiplier); + } + + public function divide($divisor): static + { + return $this->newInstance($this->amount / $divisor); + } + + private function newInstance($amount): static + { + return new self($amount); + } + + public function isZero(): bool + { + return $this->amount == 0; + } + + public function __toString() + { + return (string) $this->amount; + } +} diff --git a/tests/CartTest.php b/tests/CartTest.php index fc91a2b..b4dd8b7 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -139,8 +139,8 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total())->toBe(530) - ->and(Cart::subtotal())->toBe(530) + ->and(Cart::total()->amount())->toBe(530.0) + ->and(Cart::subtotal()->amount())->toBe(530.0) ->and(Cart::weight())->toBe(10); }); @@ -160,7 +160,7 @@ expect(Cart::isEmpty())->toBeTrue() ->and(Cart::items())->toHaveCount(0) - ->and(Cart::total())->toBe(0) + ->and(Cart::total()->amount())->toBe(0.0) ->and(Cart::weight())->toBe(0); }); @@ -180,7 +180,7 @@ Cart::add(new CartItem($product2, 1)); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total())->toBe(530) + ->and(Cart::total()->amount())->toBe(530.0) ->and(Cart::quantity())->toBe(3) ->and(Cart::weight())->toBe(10); @@ -189,8 +189,8 @@ Cart::remove($cartItem->hash()); expect(Cart::items())->toHaveCount(1) - ->and(Cart::total())->toBe(60) - ->and(Cart::subtotal())->toBe(60) + ->and(Cart::total()->amount())->toBe(60.0) + ->and(Cart::subtotal()->amount())->toBe(60.0) ->and(Cart::quantity())->toBe(2) ->and(Cart::weight())->toBe(2); }); @@ -212,7 +212,7 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total())->toBe(530) + ->and(Cart::total()->amount())->toBe(530.0) ->and(Cart::quantity())->toBe(3) ->and(Cart::weight())->toBe(10); @@ -227,7 +227,7 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total())->toBe(1510) + ->and(Cart::total()->amount())->toBe(1510.0) ->and(Cart::quantity())->toBe(12) ->and(Cart::weight())->toBe(20); }); @@ -267,7 +267,7 @@ expect(Cart::item($item->hash))->toBeInstanceOf(\Ozdemir\Aurora\CartItem::class) ->and(Cart::item($item->hash)->model->id)->toBe(3) ->and(Cart::item($item->hash)->quantity)->toBe(3) - ->and(Cart::item($item->hash)->unitPrice())->toBe(30) + ->and(Cart::item($item->hash)->unitPrice()->amount())->toBe(30.0) ->and(Cart::item($item->hash)->weight())->toBe(3) ->and(Cart::item($item->hash)->options)->toBeInstanceOf(Collection::class) ->and(Cart::item($item->hash)->options)->toHaveCount(2) @@ -341,7 +341,7 @@ ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) - ->and(Cart::total())->toBe(0) + ->and(Cart::total()->amount())->toBe(0.0) ->and(Cart::getInstanceKey())->toBe('cart'); }); @@ -381,7 +381,7 @@ ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) - ->and(Cart::total())->toBe(0) + ->and(Cart::total()->amount())->toBe(0.0) ->and(Cart::getInstanceKey())->toBe('cart'); Cart::loadSession($oldSession); diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index 47cfd82..24aed41 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -44,8 +44,8 @@ // restore the cart from snapshot Cart::restore($snapshot); - expect(Cart::subtotal())->toBe(156) - ->and(Cart::total())->toBe(156) + expect(Cart::subtotal()->amount())->toBe(156.0) + ->and(Cart::total()->amount())->toBe(156.0) ->and(Cart::items()->count())->toBe(1) ->and(Cart::quantity())->toBe(3); }); From c71e993d2fb2491dd63bd8bfa56efacf40fada82 Mon Sep 17 00:00:00 2001 From: n1crack Date: Wed, 1 Nov 2023 14:47:03 +0000 Subject: [PATCH 03/36] Fix styling --- src/Cart.php | 2 +- src/CartItem.php | 2 +- src/Contracts/CartItemInterface.php | 1 - src/Money.php | 2 +- src/Traits/SellableTrait.php | 1 - tests/CartTest.php | 2 +- tests/ItemTest.php | 2 +- tests/Models/Product.php | 1 - tests/TestCase.php | 1 - tests/ValidationTest.php | 1 - 10 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Cart.php b/src/Cart.php index c98c335..f11c20a 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -15,7 +15,7 @@ public function __construct(readonly public StorageInterface $storage) { $storage_session_key = config('cart.storage.session_key'); - $this->sessionKey = (new $storage_session_key)(); + $this->sessionKey = (new $storage_session_key())(); $this->items = $this->getStorage('items') ?? new CartItemCollection(); } diff --git a/src/CartItem.php b/src/CartItem.php index 69adef0..a5fe9f1 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -3,8 +3,8 @@ namespace Ozdemir\Aurora; use Illuminate\Support\Collection; -use Ozdemir\Aurora\Contracts\Sellable; use Ozdemir\Aurora\Contracts\CartItemInterface; +use Ozdemir\Aurora\Contracts\Sellable; class CartItem implements CartItemInterface { diff --git a/src/Contracts/CartItemInterface.php b/src/Contracts/CartItemInterface.php index 3605a32..71d645b 100644 --- a/src/Contracts/CartItemInterface.php +++ b/src/Contracts/CartItemInterface.php @@ -27,5 +27,4 @@ public function unitPrice(); public function subtotal(); public function total(); - } diff --git a/src/Money.php b/src/Money.php index 89dd4f2..19d25f2 100644 --- a/src/Money.php +++ b/src/Money.php @@ -17,7 +17,7 @@ public function round($precision = null, $mode = PHP_ROUND_HALF_UP): static { if (is_null($precision)) { $precision = 2; -// $precision = config('cart.precision'); + // $precision = config('cart.precision'); } $amount = round($this->amount, $precision, $mode); diff --git a/src/Traits/SellableTrait.php b/src/Traits/SellableTrait.php index 01b8abc..a11ab55 100644 --- a/src/Traits/SellableTrait.php +++ b/src/Traits/SellableTrait.php @@ -4,7 +4,6 @@ trait SellableTrait { - public function cartItemId() { return $this->id; diff --git a/tests/CartTest.php b/tests/CartTest.php index b4dd8b7..208df12 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -287,7 +287,7 @@ expect(Cart::items())->toHaveCount(1); - $wishlist = Cart::make( new \Ozdemir\Aurora\Storages\ArrayStorage('wishlist')); + $wishlist = Cart::make(new \Ozdemir\Aurora\Storages\ArrayStorage('wishlist')); expect($wishlist->items())->toHaveCount(0); diff --git a/tests/ItemTest.php b/tests/ItemTest.php index 96f9404..660d95b 100644 --- a/tests/ItemTest.php +++ b/tests/ItemTest.php @@ -5,7 +5,7 @@ use Ozdemir\Aurora\Facades\Cart; it('can return cart item class when a new item added', function() { - + expect(Cart::isEmpty())->toBeTrue(); $product = new \Ozdemir\Aurora\Tests\Models\Product(); diff --git a/tests/Models/Product.php b/tests/Models/Product.php index 46c0bf6..aae9b2b 100644 --- a/tests/Models/Product.php +++ b/tests/Models/Product.php @@ -2,7 +2,6 @@ namespace Ozdemir\Aurora\Tests\Models; - use Ozdemir\Aurora\Contracts\Sellable; use Ozdemir\Aurora\Traits\SellableTrait; diff --git a/tests/TestCase.php b/tests/TestCase.php index a75c3ac..524251d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,6 @@ namespace Ozdemir\Aurora\Tests; -use Illuminate\Events\Dispatcher; use Orchestra\Testbench\TestCase as Orchestra; use Ozdemir\Aurora\CartServiceProvider; use Ozdemir\Aurora\Storages\ArrayStorage; diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 1470bd5..536aa85 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -1,6 +1,5 @@ Date: Wed, 1 Nov 2023 21:09:41 +0300 Subject: [PATCH 04/36] v2 3rd part, add calculations --- src/Calculator.php | 15 ++ src/Cart.php | 106 ++++++++-- src/CartCalculatorCollection.php | 15 ++ src/CartItem.php | 20 +- src/CartItemCollection.php | 2 +- src/CartServiceProvider.php | 24 ++- src/Contracts/CartItemInterface.php | 3 - .../CartStorage.php} | 4 +- src/Enums/CartCalculator.php | 10 + src/Enums/CartItemCalculator.php | 10 + src/Money.php | 27 ++- src/Storages/ArrayStorage.php | 4 +- src/Storages/CacheStorage.php | 4 +- src/Storages/SessionStorage.php | 4 +- tests/CalculatorTest.php | 194 ++++++++++++++++++ tests/SerializeTest.php | 2 +- tests/TestCase.php | 61 ++++++ 17 files changed, 461 insertions(+), 44 deletions(-) create mode 100644 src/Calculator.php create mode 100644 src/CartCalculatorCollection.php rename src/{Storages/StorageInterface.php => Contracts/CartStorage.php} (82%) create mode 100644 src/Enums/CartCalculator.php create mode 100644 src/Enums/CartItemCalculator.php create mode 100644 tests/CalculatorTest.php diff --git a/src/Calculator.php b/src/Calculator.php new file mode 100644 index 0000000..10341af --- /dev/null +++ b/src/Calculator.php @@ -0,0 +1,15 @@ +send([$price, []]) + ->through($pipeline) + ->thenReturn(); + } +} diff --git a/src/Cart.php b/src/Cart.php index c98c335..dad66f8 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -3,24 +3,30 @@ namespace Ozdemir\Aurora; use Ozdemir\Aurora\Contracts\CartItemInterface; -use Ozdemir\Aurora\Storages\StorageInterface; +use Ozdemir\Aurora\Contracts\CartStorage; +use Ozdemir\Aurora\Enums\CartCalculator; +use Ozdemir\Aurora\Enums\CartItemCalculator; class Cart { - public CartItemCollection $items; - private string $sessionKey; - public function __construct(readonly public StorageInterface $storage) + private CartCalculatorCollection $pipeline; + + public CartItemCollection $items; + + public function __construct(readonly public CartStorage $storage) { $storage_session_key = config('cart.storage.session_key'); $this->sessionKey = (new $storage_session_key)(); - $this->items = $this->getStorage('items') ?? new CartItemCollection(); + $this->pipeline = app(CartCalculatorCollection::class); + + $this->load(); } - public function make(StorageInterface $storage): static + public function make(CartStorage $storage): static { return new static($storage); } @@ -35,7 +41,7 @@ public function add(CartItemInterface ...$cartItems): void foreach ($cartItems as $cartItem) { $this->items->updateOrAdd($cartItem); } - $this->updateStorage('items', $this->items); + $this->save(); } public function sync(CartItemInterface ...$cartItems): void @@ -43,12 +49,13 @@ public function sync(CartItemInterface ...$cartItems): void $this->clear(); $this->add(...$cartItems); - $this->updateStorage('items', $this->items); + + $this->save(); } public function item(string $hash): CartItemInterface { - // todo: throw NotFound exception + // todo: throw NotFoundException return $this->items->get($hash); } @@ -56,18 +63,29 @@ public function update(string $hash, int $quantity): ?CartItemInterface { return tap($this->item($hash), function(CartItemInterface $cartItem) use ($quantity) { $cartItem->quantity = $quantity; - $this->updateStorage('items', $this->items); + + $this->save(); }); } public function subtotal(): Money { - return $this->items->subtotal(); + [$subtotal, $breakdowns] = Calculator::calculate( + $this->items->subtotal(), + $this->pipeline[CartCalculator::SUBTOTAL->value] ?? [] + ); + + return $subtotal->setBreakdowns($breakdowns)->round(); } public function total(): Money { - return $this->subtotal()->round(); + [$total, $breakdowns] = Calculator::calculate( + $this->subtotal(), + $this->pipeline[CartCalculator::TOTAL->value] ?? [] + ); + + return $total->setBreakdowns($breakdowns)->round(); } public function weight(): float|int @@ -78,13 +96,15 @@ public function weight(): float|int public function remove($hash): void { $this->items->forget($hash); - $this->updateStorage('items', $this->items); + + $this->save(); } public function clear(): void { $this->items = new CartItemCollection(); - $this->updateStorage('items', $this->items); + + $this->save(); } public function isEmpty(): bool @@ -102,11 +122,16 @@ public function quantity(): int return $this->items->sum('quantity'); } - public function loadSession($sessionKey) + public function count(): int + { + return $this->items->count(); + } + + public function loadSession($sessionKey): void { $this->sessionKey = $sessionKey; - $this->items = $this->getStorage('items') ?? new CartItemCollection(); + $this->load(); } public function getSessionKey(): string @@ -114,7 +139,19 @@ public function getSessionKey(): string return $this->sessionKey; } - protected function updateStorage(string $key, mixed $data): void + private function save(): void + { + $this->putStorage('items', $this->items); + } + + private function load(): void + { + $this->items = $this->getStorage('items') ?? new CartItemCollection(); + + $this->pipeline = $this->pipeline->reload($this->getStorage('pipeline')); + } + + protected function putStorage(string $key, mixed $data): void { $this->storage->put($this->getSessionKey() . '.' . $key, $data); } @@ -129,12 +166,45 @@ public function snapshot(): string return serialize($this); } - public function restore(string $string): static + public function rollback(string $string): static { $cart = unserialize($string); $this->items = $cart->items(); + $this->pipeline = $this->pipeline->reload($cart->calculators()); + return $this; } + + public function calculators(): CartCalculatorCollection + { + return $this->pipeline; + } + + public function calculateUsing(CartCalculator $target, array $pipeline): void + { + $this->pipeline[$target->value] = $pipeline; + + $this->putStorage('pipeline', $this->pipeline); + } + + public function calculateItemsUsing(CartItemCalculator $target, array $pipeline): void + { + $this->pipeline[$target->value] = $pipeline; + + $this->putStorage('pipeline', $this->pipeline); + $this->putStorage('items', $this->items); + } + + public function instance(): array + { + return [ + 'subtotal' => $this->subtotal(), + 'total' => $this->total(), + 'item_breakdowns' => [], + 'cart_breakdowns' => [], + 'meta' => [], + ]; + } } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php new file mode 100644 index 0000000..16ecec3 --- /dev/null +++ b/src/CartCalculatorCollection.php @@ -0,0 +1,15 @@ +items = $items; + + return $this; + } +} diff --git a/src/CartItem.php b/src/CartItem.php index 69adef0..f8740f9 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\Sellable; use Ozdemir\Aurora\Contracts\CartItemInterface; +use Ozdemir\Aurora\Enums\CartItemCalculator; class CartItem implements CartItemInterface { @@ -85,11 +86,20 @@ public function unitPrice(): Money public function subtotal(): Money { - return $this->unitPrice()->multiply($this->quantity); - } + $pipeline = app(CartCalculatorCollection::class); - public function total(): Money - { - return $this->subtotal(); + $calculators = collect($pipeline[CartItemCalculator::SUBTOTAL->value] ?? [])->filter(fn ($values) => in_array($this->model->id, $values)); + + $subtotal = $this->unitPrice()->multiply($this->quantity); + + if ($calculators->count()) { + [$subtotal, $breakdowns] = Calculator::calculate( + $this->unitPrice()->multiply($this->quantity), + $calculators->keys()->toArray() + ); + + return $subtotal->setBreakdowns($breakdowns)->round(); + } + return $subtotal->round(); } } diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php index 917aefa..2976109 100644 --- a/src/CartItemCollection.php +++ b/src/CartItemCollection.php @@ -31,7 +31,7 @@ public function updateOrAdd(CartItem $cartItem): CartItem public function subtotal(): Money { return new Money( - $this->sum(fn (CartItemInterface $cartItem) => $cartItem->total()->amount()) + $this->sum(fn (CartItemInterface $cartItem) => $cartItem->subtotal()->amount()) ); } } diff --git a/src/CartServiceProvider.php b/src/CartServiceProvider.php index 13f9cb5..a800b77 100644 --- a/src/CartServiceProvider.php +++ b/src/CartServiceProvider.php @@ -3,7 +3,7 @@ namespace Ozdemir\Aurora; use Illuminate\Support\ServiceProvider; -use Ozdemir\Aurora\Storages\StorageInterface; +use Ozdemir\Aurora\Contracts\CartStorage; class CartServiceProvider extends ServiceProvider { @@ -21,10 +21,30 @@ public function register() $storageClass = config('cart.storage'); - /* @var StorageInterface $storage */ + /* @var CartStorage $storage */ $storage = new $storageClass($defaultInstance); return new $cartClass($storage); }); + + $this->app->singleton(CartCalculatorCollection::class, function($app) { + return new CartCalculatorCollection; + }); + +// \Ozdemir\Aurora\Facades\Cart::calculateUsing( +// CartCalculator::SUBTOTAL, +// [ +// Tax::class, +// ShippingClass::class +// ], +// ); +// +// \Ozdemir\Aurora\Facades\Cart::calculateUsing( +// CartCalculator::TOTAL, +// [ +// Tax::class, +// ShippingClass::class +// ], +// ); } } diff --git a/src/Contracts/CartItemInterface.php b/src/Contracts/CartItemInterface.php index 3605a32..88d81e3 100644 --- a/src/Contracts/CartItemInterface.php +++ b/src/Contracts/CartItemInterface.php @@ -25,7 +25,4 @@ public function optionPrice(); public function unitPrice(); public function subtotal(); - - public function total(); - } diff --git a/src/Storages/StorageInterface.php b/src/Contracts/CartStorage.php similarity index 82% rename from src/Storages/StorageInterface.php rename to src/Contracts/CartStorage.php index a440d1c..b891c64 100644 --- a/src/Storages/StorageInterface.php +++ b/src/Contracts/CartStorage.php @@ -1,8 +1,8 @@ amount; } + public function breakdowns() + { + return $this->breakdowns; + } + + public function setBreakdowns($breakdowns): static + { + $this->breakdowns = $breakdowns; + + return new self($this->amount, $breakdowns); + } + public function round($precision = null, $mode = PHP_ROUND_HALF_UP): static { - if (is_null($precision)) { - $precision = 2; -// $precision = config('cart.precision'); - } + $precision ??= config('cart.precision'); $amount = round($this->amount, $precision, $mode); - return $this->newInstance($amount); + return $this->newInstance($amount, $this->breakdowns); } public function add(self $addend): static @@ -45,9 +54,9 @@ public function divide($divisor): static return $this->newInstance($this->amount / $divisor); } - private function newInstance($amount): static + private function newInstance($amount, $breakdowns = []): static { - return new self($amount); + return new self($amount, $breakdowns); } public function isZero(): bool @@ -57,6 +66,6 @@ public function isZero(): bool public function __toString() { - return (string) $this->amount; + return (string)$this->amount; } } diff --git a/src/Storages/ArrayStorage.php b/src/Storages/ArrayStorage.php index e2d8840..9106af8 100644 --- a/src/Storages/ArrayStorage.php +++ b/src/Storages/ArrayStorage.php @@ -2,7 +2,9 @@ namespace Ozdemir\Aurora\Storages; -class ArrayStorage implements StorageInterface +use Ozdemir\Aurora\Contracts\CartStorage; + +class ArrayStorage implements CartStorage { public string $instance; diff --git a/src/Storages/CacheStorage.php b/src/Storages/CacheStorage.php index b64dcca..49ccb44 100644 --- a/src/Storages/CacheStorage.php +++ b/src/Storages/CacheStorage.php @@ -2,7 +2,9 @@ namespace Ozdemir\Aurora\Storages; -class CacheStorage implements StorageInterface +use Ozdemir\Aurora\Contracts\CartStorage; + +class CacheStorage implements CartStorage { public string $instance; diff --git a/src/Storages/SessionStorage.php b/src/Storages/SessionStorage.php index 90bdd9a..982b88f 100644 --- a/src/Storages/SessionStorage.php +++ b/src/Storages/SessionStorage.php @@ -2,7 +2,9 @@ namespace Ozdemir\Aurora\Storages; -class SessionStorage implements StorageInterface +use Ozdemir\Aurora\Contracts\CartStorage; + +class SessionStorage implements CartStorage { public string $instance; diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php new file mode 100644 index 0000000..bf90d15 --- /dev/null +++ b/tests/CalculatorTest.php @@ -0,0 +1,194 @@ +id = 3; + $product->price = 30; + + Cart::add( + new CartItem($product, quantity: 1), + ); + + expect(Cart::quantity())->toBe(1) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::calculators()->toArray())->toBe( + [ + \Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value => [ + \Ozdemir\Aurora\Tests\ShippingExample::class + ] + ] + ) + ->and(Cart::total()->amount())->toBe(40.0); +}); + + +it('can use multiple calculators', function() { + Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + \Ozdemir\Aurora\Tests\ShippingExample::class, // +10 + \Ozdemir\Aurora\Tests\TaxExample::class, // +15 + ]); + + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + + Cart::add( + new CartItem($product, quantity: 1), + ); + + expect(Cart::quantity())->toBe(1) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::total()->amount())->toBe(55.0); +}); + +it('can have breakdowns', function() { + Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + \Ozdemir\Aurora\Tests\ShippingExample::class, // +10 + \Ozdemir\Aurora\Tests\TaxExample::class, // +15 + ]); + + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + + Cart::add( + new CartItem($product, quantity: 1), + new CartItem($product, quantity: 2), + ); + + expect(Cart::total()->amount())->toBe(115.0) + ->and(Cart::total()->breakdowns())->toHaveCount(2) + ->and( + json_encode(Cart::total()->breakdowns()) + ) + ->toBe( + json_encode([ + ['label' => 'Shipping', 'value' => new Money(10)], + ['label' => 'Tax', 'value' => new Money(15)], + ]) + ); +}); + + +it('can have inline calculators', function() { + Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + function($payload, Closure $next) { + /* @var Money $price */ + [$price, $breakdowns] = $payload; + + $shippingCost = new Money(10); + + $price = $price->add($shippingCost); + + $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; + + return $next([$price, $breakdowns]); + }, + function($payload, Closure $next) { + /* @var Money $price */ + [$price, $breakdowns] = $payload; + + $taxCost = new Money(15); + + $price = $price->add($taxCost); + + $breakdowns[] = ['label' => 'Shipping', 'value' => $taxCost]; + + return $next([$price, $breakdowns]); + }, + ]); + + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 30; + + Cart::add( + new CartItem($product, quantity: 1), + new CartItem($product, quantity: 2), + ); + + expect(Cart::total()->amount())->toBe(115.0) + ->and(Cart::total()->breakdowns())->toHaveCount(2); +}); + + +it('can have calculators on cart subtotal', function() { + + Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL, [ + \Ozdemir\Aurora\Tests\DiscountExample::class // - 5% + ]); + + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 200; + + Cart::add( + new CartItem($product, quantity: 1), + ); + + expect(Cart::quantity())->toBe(1) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::calculators()->toArray())->toBe( + [ + \Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value => [ + \Ozdemir\Aurora\Tests\DiscountExample::class + ] + ] + ) + ->and(Cart::subtotal()->amount())->toBe(190.0) + ->and(Cart::total()->amount())->toBe(190.0) + ->and( + json_encode(Cart::subtotal()->breakdowns()) + ) + ->toBe( + json_encode([ + ['label' => 'Discount', 'value' => new Money(-10)], + ]) + ); +}); + + +it('can have calculators on buyable item subtotal', function() { + + Cart::calculateItemsUsing( + \Ozdemir\Aurora\Enums\CartItemCalculator::SUBTOTAL, + [ + \Ozdemir\Aurora\Tests\DiscountExample::class => [3, 6], // for the items that has these ids run the discount -5% + ] + ); + + $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product->id = 3; + $product->price = 200; + + $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2->id = 4; + $product2->price = 50; + + $product3 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product3->id = 6; + $product3->price = 100; + + Cart::add( + new CartItem($product, quantity: 1), // 200 * 0,95 = 190 + new CartItem($product2, quantity: 3), // 150 = 150 + new CartItem($product3, quantity: 1), // 100 * 0,95 = 95 + ); + + expect(Cart::quantity())->toBe(5) + ->and(Cart::count())->toBe(3) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::subtotal()->amount())->toBe(435.0) + ->and(Cart::total()->amount())->toBe(435.0); +}); + + diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index 24aed41..e1af9b4 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -42,7 +42,7 @@ expect(Cart::isEmpty())->toBeTrue(); // restore the cart from snapshot - Cart::restore($snapshot); + Cart::rollback($snapshot); expect(Cart::subtotal()->amount())->toBe(156.0) ->and(Cart::total()->amount())->toBe(156.0) diff --git a/tests/TestCase.php b/tests/TestCase.php index a75c3ac..4d459b8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,10 +2,14 @@ namespace Ozdemir\Aurora\Tests; +use Closure; use Illuminate\Events\Dispatcher; use Orchestra\Testbench\TestCase as Orchestra; +use Ozdemir\Aurora\Cart; use Ozdemir\Aurora\CartServiceProvider; +use Ozdemir\Aurora\Money; use Ozdemir\Aurora\Storages\ArrayStorage; +use function PHPUnit\Framework\once; class TestCase extends Orchestra { @@ -37,3 +41,60 @@ public function getEnvironmentSetUp($app) config()->set('database.default', 'testing'); } } + +/* @noinspection */ + +class TaxExample +{ + public function handle($payload, Closure $next) + { + /* @var Money $price */ + [$price, $breakdowns] = $payload; + + $taxPrice = new Money(15); + + $price = $price->add($taxPrice); + + $breakdowns[] = ['label' => 'Tax', 'value' => $taxPrice]; + + return $next([$price, $breakdowns]); + } +} + +/* @noinspection */ + +class ShippingExample +{ + public function handle($payload, Closure $next) + { + /* @var Cart $cart */ + /* @var Money $price */ + [$price, $breakdowns] = $payload; + + $shippingCost = new Money(10); + + $price = $price->add($shippingCost); + + $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; + + return $next([$price, $breakdowns]); + } +} + +/* @noinspection */ +class DiscountExample +{ + public function handle($payload, Closure $next) + { + /* @var Money $price */ + [$price, $breakdowns] = $payload; + + $discountPrice = new Money($price->multiply(5 / 100)->amount()); + + $price = $price->subtract($discountPrice); + + $breakdowns[] = ['label' => 'Discount', 'value' => $discountPrice->multiply(-1)]; + + return $next([$price, $breakdowns]); + } +} From 1b0157ff4ed82fd87d88cf1473bcf0a7b06f678c Mon Sep 17 00:00:00 2001 From: n1crack Date: Wed, 1 Nov 2023 18:12:43 +0000 Subject: [PATCH 05/36] Fix styling --- src/Calculator.php | 1 - src/CartItem.php | 3 ++- src/CartServiceProvider.php | 32 ++++++++++++++++---------------- tests/CalculatorTest.php | 15 ++++++--------- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Calculator.php b/src/Calculator.php index 10341af..f23087d 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -4,7 +4,6 @@ class Calculator { - public static function calculate($price, $pipeline = []) { return app('pipeline') diff --git a/src/CartItem.php b/src/CartItem.php index bf0d09c..2ab323f 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -4,8 +4,8 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; -use Ozdemir\Aurora\Enums\CartItemCalculator; use Ozdemir\Aurora\Contracts\Sellable; +use Ozdemir\Aurora\Enums\CartItemCalculator; class CartItem implements CartItemInterface { @@ -100,6 +100,7 @@ public function subtotal(): Money return $subtotal->setBreakdowns($breakdowns)->round(); } + return $subtotal->round(); } } diff --git a/src/CartServiceProvider.php b/src/CartServiceProvider.php index a800b77..7038c3d 100644 --- a/src/CartServiceProvider.php +++ b/src/CartServiceProvider.php @@ -28,23 +28,23 @@ public function register() }); $this->app->singleton(CartCalculatorCollection::class, function($app) { - return new CartCalculatorCollection; + return new CartCalculatorCollection(); }); -// \Ozdemir\Aurora\Facades\Cart::calculateUsing( -// CartCalculator::SUBTOTAL, -// [ -// Tax::class, -// ShippingClass::class -// ], -// ); -// -// \Ozdemir\Aurora\Facades\Cart::calculateUsing( -// CartCalculator::TOTAL, -// [ -// Tax::class, -// ShippingClass::class -// ], -// ); + // \Ozdemir\Aurora\Facades\Cart::calculateUsing( + // CartCalculator::SUBTOTAL, + // [ + // Tax::class, + // ShippingClass::class + // ], + // ); + // + // \Ozdemir\Aurora\Facades\Cart::calculateUsing( + // CartCalculator::TOTAL, + // [ + // Tax::class, + // ShippingClass::class + // ], + // ); } } diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index bf90d15..bf0490e 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -4,10 +4,9 @@ use Ozdemir\Aurora\Facades\Cart; use Ozdemir\Aurora\Money; - it('can use calculators', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ - \Ozdemir\Aurora\Tests\ShippingExample::class + \Ozdemir\Aurora\Tests\ShippingExample::class, ]); $product = new \Ozdemir\Aurora\Tests\Models\Product(); @@ -23,8 +22,8 @@ ->and(Cart::calculators()->toArray())->toBe( [ \Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value => [ - \Ozdemir\Aurora\Tests\ShippingExample::class - ] + \Ozdemir\Aurora\Tests\ShippingExample::class, + ], ] ) ->and(Cart::total()->amount())->toBe(40.0); @@ -124,7 +123,7 @@ function($payload, Closure $next) { it('can have calculators on cart subtotal', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL, [ - \Ozdemir\Aurora\Tests\DiscountExample::class // - 5% + \Ozdemir\Aurora\Tests\DiscountExample::class, // - 5% ]); $product = new \Ozdemir\Aurora\Tests\Models\Product(); @@ -140,8 +139,8 @@ function($payload, Closure $next) { ->and(Cart::calculators()->toArray())->toBe( [ \Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value => [ - \Ozdemir\Aurora\Tests\DiscountExample::class - ] + \Ozdemir\Aurora\Tests\DiscountExample::class, + ], ] ) ->and(Cart::subtotal()->amount())->toBe(190.0) @@ -190,5 +189,3 @@ function($payload, Closure $next) { ->and(Cart::subtotal()->amount())->toBe(435.0) ->and(Cart::total()->amount())->toBe(435.0); }); - - From 1a621686f1067c91f585bcb01bc20c1541ac96c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Wed, 1 Nov 2023 22:29:13 +0300 Subject: [PATCH 06/36] refactor calculator class --- src/Calculator.php | 20 +++++++++++++++++++- src/CartCalculatorCollection.php | 2 ++ tests/TestCase.php | 8 ++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Calculator.php b/src/Calculator.php index 10341af..de20e44 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -4,12 +4,30 @@ class Calculator { + public $skipCalculation = null; public static function calculate($price, $pipeline = []) { + $skip = app(CartCalculatorCollection::class)->skipped; + return app('pipeline') ->send([$price, []]) - ->through($pipeline) + ->through(collect($pipeline)->reject(fn ($value) => $value === $skip)->toArray()) ->thenReturn(); } + + public static function skip($class, $callback): mixed + { + $calcCollection = app(CartCalculatorCollection::class); + + $calcCollection->skipped = is_string($class) ? $class : get_class($class); + + $value = $callback(); + + $calcCollection->skipped = null; + + return $value; + } + + } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php index 16ecec3..8f84753 100644 --- a/src/CartCalculatorCollection.php +++ b/src/CartCalculatorCollection.php @@ -6,6 +6,8 @@ class CartCalculatorCollection extends Collection { + public $skipped = null; + public function reload($items = []): static { $this->items = $items; diff --git a/tests/TestCase.php b/tests/TestCase.php index 9183845..bcd81b7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,7 @@ use Closure; use Orchestra\Testbench\TestCase as Orchestra; +use Ozdemir\Aurora\Cart; use Ozdemir\Aurora\CartServiceProvider; use Ozdemir\Aurora\Money; use Ozdemir\Aurora\Storages\ArrayStorage; @@ -79,6 +80,7 @@ public function handle($payload, Closure $next) } /* @noinspection */ + class DiscountExample { public function handle($payload, Closure $next) @@ -86,6 +88,12 @@ public function handle($payload, Closure $next) /* @var Money $price */ [$price, $breakdowns] = $payload; + $total = \Ozdemir\Aurora\Calculator::skip($this, function() { + return \Ozdemir\Aurora\Facades\Cart::total()->amount(); + }); + + dump($total); + $discountPrice = new Money($price->multiply(5 / 100)->amount()); $price = $price->subtract($discountPrice); From e745818006a7bce9d113ef3f09dbba76f0641330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Wed, 1 Nov 2023 22:43:54 +0300 Subject: [PATCH 07/36] refactor calculator --- src/Calculator.php | 19 +++++++++++-------- src/Cart.php | 2 +- src/CartCalculatorCollection.php | 2 -- src/CartItem.php | 2 +- src/CartServiceProvider.php | 4 ++-- tests/TestCase.php | 12 ++++++------ 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Calculator.php b/src/Calculator.php index de20e44..a840820 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -4,27 +4,30 @@ class Calculator { - public $skipCalculation = null; + public ?string $skip = null; - public static function calculate($price, $pipeline = []) + public CartCalculatorCollection $pipeline; + + public function __construct() { - $skip = app(CartCalculatorCollection::class)->skipped; + $this->pipeline = new CartCalculatorCollection; + } + public static function calculate($price, $calculations = []) + { return app('pipeline') ->send([$price, []]) - ->through(collect($pipeline)->reject(fn ($value) => $value === $skip)->toArray()) + ->through(collect($calculations)->reject(fn ($calculation) => $calculation === app(self::class)->skip)->toArray()) ->thenReturn(); } public static function skip($class, $callback): mixed { - $calcCollection = app(CartCalculatorCollection::class); - - $calcCollection->skipped = is_string($class) ? $class : get_class($class); + app(self::class)->skip = is_string($class) ? $class : get_class($class); $value = $callback(); - $calcCollection->skipped = null; + app(self::class)->skip = null; return $value; } diff --git a/src/Cart.php b/src/Cart.php index d23dc20..d1d4fd9 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -21,7 +21,7 @@ public function __construct(readonly public CartStorage $storage) $this->sessionKey = (new $storage_session_key())(); - $this->pipeline = app(CartCalculatorCollection::class); + $this->pipeline = app(Calculator::class)->pipeline; $this->load(); } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php index 8f84753..16ecec3 100644 --- a/src/CartCalculatorCollection.php +++ b/src/CartCalculatorCollection.php @@ -6,8 +6,6 @@ class CartCalculatorCollection extends Collection { - public $skipped = null; - public function reload($items = []): static { $this->items = $items; diff --git a/src/CartItem.php b/src/CartItem.php index bf0d09c..a5094ac 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -86,7 +86,7 @@ public function unitPrice(): Money public function subtotal(): Money { - $pipeline = app(CartCalculatorCollection::class); + $pipeline = app(Calculator::class)->pipeline; $calculators = collect($pipeline[CartItemCalculator::SUBTOTAL->value] ?? [])->filter(fn ($values) => in_array($this->model->id, $values)); diff --git a/src/CartServiceProvider.php b/src/CartServiceProvider.php index a800b77..7045fbc 100644 --- a/src/CartServiceProvider.php +++ b/src/CartServiceProvider.php @@ -27,8 +27,8 @@ public function register() return new $cartClass($storage); }); - $this->app->singleton(CartCalculatorCollection::class, function($app) { - return new CartCalculatorCollection; + $this->app->singleton(Calculator::class, function($app) { + return new Calculator(); }); // \Ozdemir\Aurora\Facades\Cart::calculateUsing( diff --git a/tests/TestCase.php b/tests/TestCase.php index bcd81b7..a021073 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,12 +87,12 @@ public function handle($payload, Closure $next) { /* @var Money $price */ [$price, $breakdowns] = $payload; - - $total = \Ozdemir\Aurora\Calculator::skip($this, function() { - return \Ozdemir\Aurora\Facades\Cart::total()->amount(); - }); - - dump($total); +// +// $total = \Ozdemir\Aurora\Calculator::skip($this, function() { +// return \Ozdemir\Aurora\Facades\Cart::total()->amount(); +// }); +// +// dump($total); $discountPrice = new Money($price->multiply(5 / 100)->amount()); From 2f28280d365e661d32ae665ff317eb9a5eed1b2d Mon Sep 17 00:00:00 2001 From: n1crack Date: Wed, 1 Nov 2023 19:45:53 +0000 Subject: [PATCH 08/36] Fix styling --- src/Calculator.php | 4 +--- tests/TestCase.php | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Calculator.php b/src/Calculator.php index a840820..7fecd9c 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -10,7 +10,7 @@ class Calculator public function __construct() { - $this->pipeline = new CartCalculatorCollection; + $this->pipeline = new CartCalculatorCollection(); } public static function calculate($price, $calculations = []) @@ -31,6 +31,4 @@ public static function skip($class, $callback): mixed return $value; } - - } diff --git a/tests/TestCase.php b/tests/TestCase.php index a021073..ab1aacd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,12 +87,12 @@ public function handle($payload, Closure $next) { /* @var Money $price */ [$price, $breakdowns] = $payload; -// -// $total = \Ozdemir\Aurora\Calculator::skip($this, function() { -// return \Ozdemir\Aurora\Facades\Cart::total()->amount(); -// }); -// -// dump($total); + // + // $total = \Ozdemir\Aurora\Calculator::skip($this, function() { + // return \Ozdemir\Aurora\Facades\Cart::total()->amount(); + // }); + // + // dump($total); $discountPrice = new Money($price->multiply(5 / 100)->amount()); From a8034b8d4014892305a17b6f5694a598be25322c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Wed, 1 Nov 2023 23:03:42 +0300 Subject: [PATCH 09/36] cleanup --- src/Money.php | 2 +- src/Traits/SellableTrait.php | 5 ----- tests/Models/Product.php | 1 - tests/TestCase.php | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Money.php b/src/Money.php index 2c04e21..cb18959 100644 --- a/src/Money.php +++ b/src/Money.php @@ -13,7 +13,7 @@ public function amount(): float|int return $this->amount; } - public function breakdowns() + public function breakdowns(): array { return $this->breakdowns; } diff --git a/src/Traits/SellableTrait.php b/src/Traits/SellableTrait.php index a11ab55..bcad876 100644 --- a/src/Traits/SellableTrait.php +++ b/src/Traits/SellableTrait.php @@ -14,11 +14,6 @@ public function cartItemPrice() return $this->price; } - public function cartItemBasePrice() - { - return $this->basePrice; - } - public function cartItemQuantity() { return $this->quantity; diff --git a/tests/Models/Product.php b/tests/Models/Product.php index aae9b2b..586f847 100644 --- a/tests/Models/Product.php +++ b/tests/Models/Product.php @@ -12,7 +12,6 @@ class Product implements Sellable public int $id; public float|int $price; - public float|int $basePrice; public int $quantity; public float|int $weight; } diff --git a/tests/TestCase.php b/tests/TestCase.php index ab1aacd..494e700 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,7 +87,7 @@ public function handle($payload, Closure $next) { /* @var Money $price */ [$price, $breakdowns] = $payload; - // + // $total = \Ozdemir\Aurora\Calculator::skip($this, function() { // return \Ozdemir\Aurora\Facades\Cart::total()->amount(); // }); From 9f5c8ef369cead0e9c5a22d5d1ddbe643d53959f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 2 Nov 2023 22:11:57 +0300 Subject: [PATCH 10/36] refactor --- src/Cart.php | 3 +- tests/CalculatorTest.php | 34 +++++++-------- tests/CartTest.php | 40 ++++++++--------- tests/ItemTest.php | 2 +- tests/SerializeTest.php | 4 +- tests/Stubs/Calculators/Discount.php | 31 ++++++++++++++ tests/Stubs/Calculators/Shipping.php | 25 +++++++++++ tests/Stubs/Calculators/Tax.php | 25 +++++++++++ tests/{ => Stubs}/Models/Product.php | 2 +- tests/TestCase.php | 64 ---------------------------- 10 files changed, 125 insertions(+), 105 deletions(-) create mode 100644 tests/Stubs/Calculators/Discount.php create mode 100644 tests/Stubs/Calculators/Shipping.php create mode 100644 tests/Stubs/Calculators/Tax.php rename tests/{ => Stubs}/Models/Product.php (86%) diff --git a/src/Cart.php b/src/Cart.php index d1d4fd9..2f07f3b 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -36,11 +36,12 @@ public function items(): CartItemCollection return $this->items; } - public function add(CartItemInterface ...$cartItems): void + public function add(CartItemInterface ...$cartItems): void { foreach ($cartItems as $cartItem) { $this->items->updateOrAdd($cartItem); } + $this->save(); } diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index bf0490e..e9b742f 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -6,10 +6,10 @@ it('can use calculators', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ - \Ozdemir\Aurora\Tests\ShippingExample::class, + \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, ]); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -22,7 +22,7 @@ ->and(Cart::calculators()->toArray())->toBe( [ \Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value => [ - \Ozdemir\Aurora\Tests\ShippingExample::class, + \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, ], ] ) @@ -32,11 +32,11 @@ it('can use multiple calculators', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ - \Ozdemir\Aurora\Tests\ShippingExample::class, // +10 - \Ozdemir\Aurora\Tests\TaxExample::class, // +15 + \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, // +10 + \Ozdemir\Aurora\Tests\Stubs\Calculators\Tax::class, // +15 ]); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -51,11 +51,11 @@ it('can have breakdowns', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ - \Ozdemir\Aurora\Tests\ShippingExample::class, // +10 - \Ozdemir\Aurora\Tests\TaxExample::class, // +15 + \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, // +10 + \Ozdemir\Aurora\Tests\Stubs\Calculators\Tax::class, // +15 ]); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -106,7 +106,7 @@ function($payload, Closure $next) { }, ]); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -123,10 +123,10 @@ function($payload, Closure $next) { it('can have calculators on cart subtotal', function() { Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL, [ - \Ozdemir\Aurora\Tests\DiscountExample::class, // - 5% + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // - 5% ]); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 200; @@ -139,7 +139,7 @@ function($payload, Closure $next) { ->and(Cart::calculators()->toArray())->toBe( [ \Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value => [ - \Ozdemir\Aurora\Tests\DiscountExample::class, + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, ], ] ) @@ -161,19 +161,19 @@ function($payload, Closure $next) { Cart::calculateItemsUsing( \Ozdemir\Aurora\Enums\CartItemCalculator::SUBTOTAL, [ - \Ozdemir\Aurora\Tests\DiscountExample::class => [3, 6], // for the items that has these ids run the discount -5% + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class => [3, 6], // for the items that has these ids run the discount -5% ] ); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 200; - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 4; $product2->price = 50; - $product3 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product3 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product3->id = 6; $product3->price = 100; diff --git a/tests/CartTest.php b/tests/CartTest.php index 208df12..b2c58fa 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -24,12 +24,13 @@ expect(Auth::id())->toBe($user->id) ->and(Auth::check())->toBe(true) + ->and(Cart::getSessionKey())->toBe('user:123') ->and(Cart::getInstanceKey())->toBe('cart'); }); it('can add items to Cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -38,15 +39,16 @@ ); expect(Cart::quantity())->toBe(1) + ->and(Cart::total()->amount())->toBe(30.0) ->and(Cart::isEmpty())->toBeFalse(); }); it('can add multiple items to Cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 4; $product2->price = 179; @@ -61,7 +63,7 @@ it('increase items count if the id is same', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; @@ -79,7 +81,7 @@ it('can have options and options have to be instance of CartItemOption Class', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; @@ -97,7 +99,7 @@ }); it('can add new item instance with the same item id if the item has different options', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 0; @@ -120,7 +122,7 @@ }); it('can sum total cart item prices', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 1; @@ -129,7 +131,7 @@ (new CartItem($product, quantity: 2)) ); - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 2; $product2->price = 470; $product2->weight = 8; @@ -145,7 +147,7 @@ }); it('can clear the cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 1; @@ -165,14 +167,14 @@ }); it('can remove item from the cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 1; Cart::add(new CartItem($product, 2)); - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 2; $product2->price = 470; $product2->weight = 8; @@ -196,12 +198,12 @@ }); it('can sync the items', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 1; - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 2; $product2->price = 470; $product2->weight = 8; @@ -233,7 +235,7 @@ }); it('can update the quantity', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 1; $product->price = 30; $product->weight = 1; @@ -251,7 +253,7 @@ }); it('can get items from hash', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; $product->weight = 1; @@ -278,7 +280,7 @@ }); it('can initialize a new instance', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 2; $product->price = 30; $product->weight = 4; @@ -291,7 +293,7 @@ expect($wishlist->items())->toHaveCount(0); - $product2 = new \Ozdemir\Aurora\Tests\Models\Product(); + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product2->id = 3; $product2->price = 20; $product2->weight = 5; @@ -311,7 +313,7 @@ it('can refresh user id after login', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 2; $product->price = 30; $product->weight = 4; @@ -348,7 +350,7 @@ it('can load any session Cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 2; $product->price = 30; $product->weight = 4; diff --git a/tests/ItemTest.php b/tests/ItemTest.php index 660d95b..2a765d5 100644 --- a/tests/ItemTest.php +++ b/tests/ItemTest.php @@ -8,7 +8,7 @@ expect(Cart::isEmpty())->toBeTrue(); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 100; diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index e1af9b4..93d8d7c 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -5,7 +5,7 @@ it('can snapshot the current cart', function() { - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 30; $product->weight = 1; @@ -20,7 +20,7 @@ it('can unserialize from serialized string', function() { expect(Cart::isEmpty())->toBeTrue(); - $product = new \Ozdemir\Aurora\Tests\Models\Product(); + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; $product->price = 40; $product->weight = 1; diff --git a/tests/Stubs/Calculators/Discount.php b/tests/Stubs/Calculators/Discount.php new file mode 100644 index 0000000..676e454 --- /dev/null +++ b/tests/Stubs/Calculators/Discount.php @@ -0,0 +1,31 @@ +amount(); + // }); + // + // dump($total); + + $discountPrice = new Money($price->multiply(5 / 100)->amount()); + + $price = $price->subtract($discountPrice); + + $breakdowns[] = ['label' => 'Discount', 'value' => $discountPrice->multiply(-1)]; + + return $next([$price, $breakdowns]); + } +} diff --git a/tests/Stubs/Calculators/Shipping.php b/tests/Stubs/Calculators/Shipping.php new file mode 100644 index 0000000..38b5420 --- /dev/null +++ b/tests/Stubs/Calculators/Shipping.php @@ -0,0 +1,25 @@ +add($shippingCost); + + $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; + + return $next([$price, $breakdowns]); + } +} diff --git a/tests/Stubs/Calculators/Tax.php b/tests/Stubs/Calculators/Tax.php new file mode 100644 index 0000000..d37d07b --- /dev/null +++ b/tests/Stubs/Calculators/Tax.php @@ -0,0 +1,25 @@ +add($taxPrice); + + $breakdowns[] = ['label' => 'Tax', 'value' => $taxPrice]; + + return $next([$price, $breakdowns]); + } +} diff --git a/tests/Models/Product.php b/tests/Stubs/Models/Product.php similarity index 86% rename from tests/Models/Product.php rename to tests/Stubs/Models/Product.php index 586f847..0eed045 100644 --- a/tests/Models/Product.php +++ b/tests/Stubs/Models/Product.php @@ -1,6 +1,6 @@ set('database.default', 'testing'); } } - -/* @noinspection */ - -class TaxExample -{ - public function handle($payload, Closure $next) - { - /* @var Money $price */ - [$price, $breakdowns] = $payload; - - $taxPrice = new Money(15); - - $price = $price->add($taxPrice); - - $breakdowns[] = ['label' => 'Tax', 'value' => $taxPrice]; - - return $next([$price, $breakdowns]); - } -} - -/* @noinspection */ - -class ShippingExample -{ - public function handle($payload, Closure $next) - { - /* @var Cart $cart */ - /* @var Money $price */ - [$price, $breakdowns] = $payload; - - $shippingCost = new Money(10); - - $price = $price->add($shippingCost); - - $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; - - return $next([$price, $breakdowns]); - } -} - -/* @noinspection */ - -class DiscountExample -{ - public function handle($payload, Closure $next) - { - /* @var Money $price */ - [$price, $breakdowns] = $payload; - - // $total = \Ozdemir\Aurora\Calculator::skip($this, function() { - // return \Ozdemir\Aurora\Facades\Cart::total()->amount(); - // }); - // - // dump($total); - - $discountPrice = new Money($price->multiply(5 / 100)->amount()); - - $price = $price->subtract($discountPrice); - - $breakdowns[] = ['label' => 'Discount', 'value' => $discountPrice->multiply(-1)]; - - return $next([$price, $breakdowns]); - } -} From 2754b5e0a4a36687a6d9e7c459cfc529f065a7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 2 Nov 2023 22:40:01 +0300 Subject: [PATCH 11/36] refactor cartItem subtotal calculators --- src/Cart.php | 15 +++++--- src/CartItem.php | 15 +++++--- src/Money.php | 2 +- tests/CalculatorTest.php | 77 +++++++++++++++++++++++++++++++++------- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/Cart.php b/src/Cart.php index 2f07f3b..01fd470 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -183,16 +183,23 @@ public function calculators(): CartCalculatorCollection return $this->pipeline; } - public function calculateUsing(CartCalculator $target, array $pipeline): void + public function calculateTotalUsing(array $pipeline): void { - $this->pipeline[$target->value] = $pipeline; + $this->pipeline[CartCalculator::TOTAL->value] = $pipeline; $this->putStorage('pipeline', $this->pipeline); } - public function calculateItemsUsing(CartItemCalculator $target, array $pipeline): void + public function calculateSubtotalUsing(array $pipeline): void { - $this->pipeline[$target->value] = $pipeline; + $this->pipeline[CartCalculator::SUBTOTAL->value] = $pipeline; + + $this->putStorage('pipeline', $this->pipeline); + } + + public function calculateItemSubtotalUsing(array $pipeline): void + { + $this->pipeline[CartItemCalculator::SUBTOTAL->value] = $pipeline; $this->putStorage('pipeline', $this->pipeline); $this->putStorage('items', $this->items); diff --git a/src/CartItem.php b/src/CartItem.php index 43e0d13..1d4424a 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -86,16 +86,23 @@ public function unitPrice(): Money public function subtotal(): Money { - $pipeline = app(Calculator::class)->pipeline; + $calculatorArray = app(Calculator::class)->pipeline; - $calculators = collect($pipeline[CartItemCalculator::SUBTOTAL->value] ?? [])->filter(fn ($values) => in_array($this->model->id, $values)); + $subtotalCalculators = $calculatorArray[CartItemCalculator::SUBTOTAL->value] ?? []; + + if (array_is_list($subtotalCalculators)) { + $pipeline = $subtotalCalculators; + } else { + $calculators = collect($subtotalCalculators)->filter(fn ($values) => in_array($this->model->id, $values)); + $pipeline = $calculators->keys()->toArray(); + } $subtotal = $this->unitPrice()->multiply($this->quantity); - if ($calculators->count()) { + if (count($pipeline)) { [$subtotal, $breakdowns] = Calculator::calculate( $this->unitPrice()->multiply($this->quantity), - $calculators->keys()->toArray() + $pipeline ); return $subtotal->setBreakdowns($breakdowns)->round(); diff --git a/src/Money.php b/src/Money.php index cb18959..4eab765 100644 --- a/src/Money.php +++ b/src/Money.php @@ -27,7 +27,7 @@ public function setBreakdowns($breakdowns): static public function round($precision = null, $mode = PHP_ROUND_HALF_UP): static { - $precision ??= config('cart.precision'); + $precision ??= config('cart.currency.precision'); $amount = round($this->amount, $precision, $mode); diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index e9b742f..53d0ad1 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -5,7 +5,7 @@ use Ozdemir\Aurora\Money; it('can use calculators', function() { - Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + Cart::calculateTotalUsing([ \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, ]); @@ -31,7 +31,7 @@ it('can use multiple calculators', function() { - Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + Cart::calculateTotalUsing([ \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, // +10 \Ozdemir\Aurora\Tests\Stubs\Calculators\Tax::class, // +15 ]); @@ -50,7 +50,7 @@ }); it('can have breakdowns', function() { - Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + Cart::calculateTotalUsing([ \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, // +10 \Ozdemir\Aurora\Tests\Stubs\Calculators\Tax::class, // +15 ]); @@ -79,7 +79,7 @@ it('can have inline calculators', function() { - Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL, [ + Cart::calculateTotalUsing([ function($payload, Closure $next) { /* @var Money $price */ [$price, $breakdowns] = $payload; @@ -122,7 +122,7 @@ function($payload, Closure $next) { it('can have calculators on cart subtotal', function() { - Cart::calculateUsing(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL, [ + Cart::calculateSubtotalUsing([ \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // - 5% ]); @@ -156,14 +156,11 @@ function($payload, Closure $next) { }); -it('can have calculators on buyable item subtotal', function() { +it('can have calculators on sellable item subtotal', function() { - Cart::calculateItemsUsing( - \Ozdemir\Aurora\Enums\CartItemCalculator::SUBTOTAL, - [ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class => [3, 6], // for the items that has these ids run the discount -5% - ] - ); + Cart::calculateItemSubtotalUsing([ + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class => [3, 6], // for the items that has these ids run the discount -5% + ]); $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); $product->id = 3; @@ -189,3 +186,59 @@ function($payload, Closure $next) { ->and(Cart::subtotal()->amount())->toBe(435.0) ->and(Cart::total()->amount())->toBe(435.0); }); + +it('can have default calculators on subtotals of every sellable items', function() { + Cart::calculateItemSubtotalUsing([ + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // for the items that has these ids run the discount -5% + ]); + + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product->id = 3; + $product->price = 200; + + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product2->id = 4; + $product2->price = 50; + + Cart::add( + new CartItem($product, quantity: 1), // 200 * 0,95 = 190 + new CartItem($product2, quantity: 3) // 150 * 0,95 = 142,5 + ); + + expect(Cart::quantity())->toBe(4) + ->and(Cart::count())->toBe(2) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::subtotal()->amount())->toBe(332.5) + ->and(Cart::total()->amount())->toBe(332.5); +}); + +it('can have inline calculators on sellable item subtotal', function() { + Cart::calculateItemSubtotalUsing([ + \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // for the items that has these ids run the discount -5% + function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. + [$price, $breakdowns] = $payload; + $price = $price->add(new Money(0.01)); + return $next([$price, $breakdowns]); + } + ]); + + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product->id = 3; + $product->price = 200; + + $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product2->id = 4; + $product2->price = 50; + + Cart::add( + new CartItem($product, quantity: 1), // 200 * 0,95 = 190 + 0.01 + new CartItem($product2, quantity: 3) // 150 * 0,95 = 142,5 + 0.01 + ); + + expect(Cart::quantity())->toBe(4) + ->and(Cart::count())->toBe(2) + ->and(Cart::isEmpty())->toBeFalse() + ->and(Cart::subtotal()->amount())->toBe(332.52) + ->and(Cart::total()->amount())->toBe(332.52); +}); + From b7ec3d15348762bae22f90b44825f47c40e3a6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 2 Nov 2023 22:45:40 +0300 Subject: [PATCH 12/36] update calculator tests --- tests/CalculatorTest.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index 53d0ad1..0585320 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -18,6 +18,7 @@ ); expect(Cart::quantity())->toBe(1) + ->and(Cart::count())->toBe(1) ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::calculators()->toArray())->toBe( [ @@ -135,7 +136,6 @@ function($payload, Closure $next) { ); expect(Cart::quantity())->toBe(1) - ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::calculators()->toArray())->toBe( [ \Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value => [ @@ -181,8 +181,6 @@ function($payload, Closure $next) { ); expect(Cart::quantity())->toBe(5) - ->and(Cart::count())->toBe(3) - ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::subtotal()->amount())->toBe(435.0) ->and(Cart::total()->amount())->toBe(435.0); }); @@ -206,8 +204,6 @@ function($payload, Closure $next) { ); expect(Cart::quantity())->toBe(4) - ->and(Cart::count())->toBe(2) - ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::subtotal()->amount())->toBe(332.5) ->and(Cart::total()->amount())->toBe(332.5); }); @@ -218,6 +214,7 @@ function($payload, Closure $next) { function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. [$price, $breakdowns] = $payload; $price = $price->add(new Money(0.01)); + $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '0.01$']; return $next([$price, $breakdowns]); } ]); @@ -236,9 +233,8 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot ); expect(Cart::quantity())->toBe(4) - ->and(Cart::count())->toBe(2) - ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::subtotal()->amount())->toBe(332.52) - ->and(Cart::total()->amount())->toBe(332.52); + ->and(Cart::items()->first()->subtotal()->amount())->toBe(190.01) + ->and(Cart::items()->first()->subtotal()->breakdowns()[1])->toBe(['label' => 'Custom Shipping', 'value' => '0.01$']); }); From 71139a0dd935a0225b69dee09562b6c050cf2cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 2 Nov 2023 22:47:18 +0300 Subject: [PATCH 13/36] Update CartItem.php --- src/CartItem.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CartItem.php b/src/CartItem.php index 1d4424a..7602cc7 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -24,9 +24,9 @@ public function __construct(public Sellable $model, public int $quantity, public public function hash(): string { - $attr = $this->options->pluck('value')->join('-'); + $options = $this->options->pluck('value')->join('-'); - return $this->hash = md5($this->model->cartItemId() . $attr . $this->gift); + return $this->hash = md5($this->model->cartItemId() . $options . $this->gift); } public function withOption(string $name, mixed $value, float|int $price = 0, bool $percent = false, float|int $weight = 0): static From 86ad894342fdac3c9a9bd218605e7eb2ebca2f9d Mon Sep 17 00:00:00 2001 From: n1crack Date: Thu, 2 Nov 2023 19:48:34 +0000 Subject: [PATCH 14/36] Fix styling --- tests/CalculatorTest.php | 4 ++-- tests/TestCase.php | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index 0585320..92ae17b 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -215,8 +215,9 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot [$price, $breakdowns] = $payload; $price = $price->add(new Money(0.01)); $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '0.01$']; + return $next([$price, $breakdowns]); - } + }, ]); $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); @@ -237,4 +238,3 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot ->and(Cart::items()->first()->subtotal()->amount())->toBe(190.01) ->and(Cart::items()->first()->subtotal()->breakdowns()[1])->toBe(['label' => 'Custom Shipping', 'value' => '0.01$']); }); - diff --git a/tests/TestCase.php b/tests/TestCase.php index 98edf9a..34d31ea 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,11 +2,9 @@ namespace Ozdemir\Aurora\Tests; -use Closure; use Orchestra\Testbench\TestCase as Orchestra; use Ozdemir\Aurora\Cart; use Ozdemir\Aurora\CartServiceProvider; -use Ozdemir\Aurora\Money; use Ozdemir\Aurora\Storages\ArrayStorage; class TestCase extends Orchestra From 76060ddeefd466cbd04a3fd4ddde928d6511e123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Sat, 4 Nov 2023 12:26:17 +0300 Subject: [PATCH 15/36] add checksum and validation --- config/cart.php | 6 +- src/Cart.php | 20 +++- src/CartCalculatorCollection.php | 2 +- src/Contracts/CartStorage.php | 7 +- src/Generators/CheckSumGenerator.php | 16 +++ src/{ => Generators}/DefaultSessionKey.php | 2 +- src/Money.php | 4 +- src/Storages/ArrayStorage.php | 6 +- src/Storages/CacheStorage.php | 6 +- src/Storages/SessionStorage.php | 6 +- tests/CartTest.php | 8 +- tests/ChecknumTest.php | 124 +++++++++++++++++++++ 12 files changed, 179 insertions(+), 28 deletions(-) create mode 100644 src/Generators/CheckSumGenerator.php rename src/{ => Generators}/DefaultSessionKey.php (84%) create mode 100644 tests/ChecknumTest.php diff --git a/config/cart.php b/config/cart.php index 3c3a59d..a6455a4 100644 --- a/config/cart.php +++ b/config/cart.php @@ -8,7 +8,7 @@ 'storage' => [ 'class' => \Ozdemir\Aurora\Storages\SessionStorage::class, - 'session_key' => \Ozdemir\Aurora\DefaultSessionKey::class, + 'session_key' => \Ozdemir\Aurora\Generators\DefaultSessionKey::class, ], 'cache_store' => env('CART_STORE', config('cache.default')), @@ -16,6 +16,8 @@ 'currency' => [ 'class' => \Ozdemir\Aurora\Money::class, - 'precision' => 2, + 'precision' => env('CART_CURRENCY_PRECISION', 2), ], + + 'checksum_generator' => \Ozdemir\Aurora\Generators\CheckSumGenerator::class ]; diff --git a/src/Cart.php b/src/Cart.php index 01fd470..49e7704 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -147,7 +147,7 @@ private function save(): void private function load(): void { - $this->items = $this->getStorage('items') ?? new CartItemCollection(); + $this->items = $this->getStorage('items', new CartItemCollection()); $this->pipeline = $this->pipeline->reload($this->getStorage('pipeline')); } @@ -157,9 +157,9 @@ protected function putStorage(string $key, mixed $data): void $this->storage->put($this->getSessionKey() . '.' . $key, $data); } - protected function getStorage(string $key): mixed + protected function getStorage(string $key, $default = null): mixed { - return $this->storage->get($this->getSessionKey() . '.' . $key); + return $this->storage->get($this->getSessionKey() . '.' . $key, $default); } public function snapshot(): string @@ -210,9 +210,17 @@ public function instance(): array return [ 'subtotal' => $this->subtotal(), 'total' => $this->total(), - 'item_breakdowns' => [], - 'cart_breakdowns' => [], - 'meta' => [], + // todo.. breakdowns etc.. ]; } + + public function validate($clientChecksum): bool + { + return $this->checksum() === $clientChecksum; + } + + public function checksum(): string + { + return (new (config('cart.checksum_generator')))(); + } } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php index 16ecec3..b7eb041 100644 --- a/src/CartCalculatorCollection.php +++ b/src/CartCalculatorCollection.php @@ -8,7 +8,7 @@ class CartCalculatorCollection extends Collection { public function reload($items = []): static { - $this->items = $items; + $this->items = $items ?? []; return $this; } diff --git a/src/Contracts/CartStorage.php b/src/Contracts/CartStorage.php index b891c64..72a33a1 100644 --- a/src/Contracts/CartStorage.php +++ b/src/Contracts/CartStorage.php @@ -11,14 +11,15 @@ public function __construct($instance); /** * @param $key + * @param null $default * @return mixed */ - public function get($key); + public function get($key, $default = null): mixed; /** * @param $key * @param $data - * @return mixed + * @return void */ - public function put($key, $data); + public function put($key, $data): void; } diff --git a/src/Generators/CheckSumGenerator.php b/src/Generators/CheckSumGenerator.php new file mode 100644 index 0000000..ca16cac --- /dev/null +++ b/src/Generators/CheckSumGenerator.php @@ -0,0 +1,16 @@ +values()->pluck('quantity', 'hash'), + Cart::total()->amount() + ])); + } +} diff --git a/src/DefaultSessionKey.php b/src/Generators/DefaultSessionKey.php similarity index 84% rename from src/DefaultSessionKey.php rename to src/Generators/DefaultSessionKey.php index 08bfbc4..bd9082c 100644 --- a/src/DefaultSessionKey.php +++ b/src/Generators/DefaultSessionKey.php @@ -1,6 +1,6 @@ amount, $precision, $mode); diff --git a/src/Storages/ArrayStorage.php b/src/Storages/ArrayStorage.php index 9106af8..ebf6429 100644 --- a/src/Storages/ArrayStorage.php +++ b/src/Storages/ArrayStorage.php @@ -15,12 +15,12 @@ public function __construct($instance) $this->instance = $instance; } - public function get($key) + public function get($key, $default = null): mixed { - return $this->session["$this->instance.$key"] ?? null; + return $this->session["$this->instance.$key"] ?? $default; } - public function put($key, $data) + public function put($key, $data): void { $this->session["$this->instance.$key"] = $data; } diff --git a/src/Storages/CacheStorage.php b/src/Storages/CacheStorage.php index 49ccb44..7215794 100644 --- a/src/Storages/CacheStorage.php +++ b/src/Storages/CacheStorage.php @@ -13,12 +13,12 @@ public function __construct($instance) $this->instance = $instance; } - public function get($key) + public function get($key, $default = null): mixed { - return cache()->store(config('cart.cache_store'))->get("$this->instance.$key"); + return cache()->store(config('cart.cache_store'))->get("$this->instance.$key", $default); } - public function put($key, $data) + public function put($key, $data): void { cache()->store(config('cart.cache_store'))->put("$this->instance.$key", $data); } diff --git a/src/Storages/SessionStorage.php b/src/Storages/SessionStorage.php index 982b88f..7769a12 100644 --- a/src/Storages/SessionStorage.php +++ b/src/Storages/SessionStorage.php @@ -13,12 +13,12 @@ public function __construct($instance) $this->instance = $instance; } - public function get($key) + public function get($key, $default = null): mixed { - return session()->get("$this->instance.$key"); + return session()->get("$this->instance.$key", $default); } - public function put($key, $data) + public function put($key, $data): void { session()->put("$this->instance.$key", $data); } diff --git a/tests/CartTest.php b/tests/CartTest.php index b2c58fa..068068c 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -337,9 +337,9 @@ expect(Auth::id())->toBe($user->id); expect(Auth::check())->toBe(true); - Cart::loadSession('user:'. Auth::id()); + Cart::loadSession('user:' . Auth::id()); - expect(Cart::getSessionKey())->toBe('user:'. Auth::id()) + expect(Cart::getSessionKey())->toBe('user:' . Auth::id()) ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) @@ -377,9 +377,9 @@ expect(Auth::id())->toBe($user->id); expect(Auth::check())->toBe(true); - Cart::loadSession('user:'. Auth::id()); + Cart::loadSession('user:' . Auth::id()); - expect(Cart::getSessionKey())->toBe('user:'. Auth::id()) + expect(Cart::getSessionKey())->toBe('user:' . Auth::id()) ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) diff --git a/tests/ChecknumTest.php b/tests/ChecknumTest.php new file mode 100644 index 0000000..63d686d --- /dev/null +++ b/tests/ChecknumTest.php @@ -0,0 +1,124 @@ +id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); + + Cart::add(new CartItem($product, 1)); + + expect(Cart::quantity())->toBe(3) + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); + +}); + + +it('has a checknum hash and can be validated', function() { + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); + + Cart::add(new CartItem($product, 1)); + + expect(Cart::quantity())->toBe(3) + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); + + + Cart::clear(); + + expect(Cart::quantity())->toBe(0) + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); + +}); + + +it('has a checknum that can be changed if the calculators changes', function() { + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::total()->amount())->toBe(60.0) + ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); + + Cart::calculateTotalUsing([ + \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, // +10 + \Ozdemir\Aurora\Tests\Stubs\Calculators\Tax::class, // +15 + ]); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::total()->amount())->toBe(85.0) + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); + +}); + + +it('has a checknum that can be changed if the inline calculators changes', function() { + $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::total()->amount())->toBe(60.0) + ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); + + Cart::calculateTotalUsing([ + function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. + [$price, $breakdowns] = $payload; + $price = $price->add(new Money(10)); + $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '10$']; + + return $next([$price, $breakdowns]); + }, + ]); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::total()->amount())->toBe(70.0) + ->and(Cart::checksum())->toBe('19acbc85fc3a5e4d74e08f3c78275736') + ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); + + Cart::calculateTotalUsing([ + function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. + [$price, $breakdowns] = $payload; + $price = $price->add(new Money(11)); + $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '11$']; + + return $next([$price, $breakdowns]); + }, + ]); + + expect(Cart::quantity())->toBe(2) + ->and(Cart::total()->amount())->toBe(71.0) + ->and(Cart::checksum())->toBe('0ff00255271d88df533d68a60a498dff') + ->and(Cart::validate('19acbc85fc3a5e4d74e08f3c78275736'))->toBeFalse(); +}); + + + From a4a98a71362388ed53cf6d89416f74f73b9edada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Sun, 5 Nov 2023 21:57:59 +0300 Subject: [PATCH 16/36] update class names --- config/cart.php | 10 ++++------ src/Cart.php | 2 +- .../{CheckSumGenerator.php => GenerateChecksum.php} | 2 +- .../{DefaultSessionKey.php => GenerateSessionKey.php} | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) rename src/Generators/{CheckSumGenerator.php => GenerateChecksum.php} (92%) rename src/Generators/{DefaultSessionKey.php => GenerateSessionKey.php} (89%) diff --git a/config/cart.php b/config/cart.php index a6455a4..8259afa 100644 --- a/config/cart.php +++ b/config/cart.php @@ -5,11 +5,7 @@ 'cart_class' => \Ozdemir\Aurora\Cart::class, - 'storage' => [ - 'class' => \Ozdemir\Aurora\Storages\SessionStorage::class, - - 'session_key' => \Ozdemir\Aurora\Generators\DefaultSessionKey::class, - ], + 'storage' => \Ozdemir\Aurora\Storages\SessionStorage::class, 'cache_store' => env('CART_STORE', config('cache.default')), @@ -19,5 +15,7 @@ 'precision' => env('CART_CURRENCY_PRECISION', 2), ], - 'checksum_generator' => \Ozdemir\Aurora\Generators\CheckSumGenerator::class + 'session_key_generator' => \Ozdemir\Aurora\Generators\GenerateSessionKey::class, + + 'checksum_generator' => \Ozdemir\Aurora\Generators\GenerateChecksum::class ]; diff --git a/src/Cart.php b/src/Cart.php index 49e7704..e90a47b 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -17,7 +17,7 @@ class Cart public function __construct(readonly public CartStorage $storage) { - $storage_session_key = config('cart.storage.session_key'); + $storage_session_key = config('cart.session_key_generator'); $this->sessionKey = (new $storage_session_key())(); diff --git a/src/Generators/CheckSumGenerator.php b/src/Generators/GenerateChecksum.php similarity index 92% rename from src/Generators/CheckSumGenerator.php rename to src/Generators/GenerateChecksum.php index ca16cac..5cf18ac 100644 --- a/src/Generators/CheckSumGenerator.php +++ b/src/Generators/GenerateChecksum.php @@ -4,7 +4,7 @@ use Ozdemir\Aurora\Facades\Cart; -class CheckSumGenerator +class GenerateChecksum { public function __invoke(): string { diff --git a/src/Generators/DefaultSessionKey.php b/src/Generators/GenerateSessionKey.php similarity index 89% rename from src/Generators/DefaultSessionKey.php rename to src/Generators/GenerateSessionKey.php index bd9082c..95a1968 100644 --- a/src/Generators/DefaultSessionKey.php +++ b/src/Generators/GenerateSessionKey.php @@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Auth; -class DefaultSessionKey +class GenerateSessionKey { public function __invoke(): string { From c255998cac2657808a80cf97df22178bf923571e Mon Sep 17 00:00:00 2001 From: n1crack Date: Mon, 6 Nov 2023 09:56:03 +0000 Subject: [PATCH 17/36] Fix styling --- src/Generators/GenerateChecksum.php | 2 +- src/Storages/CacheStorage.php | 2 +- tests/ChecknumTest.php | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Generators/GenerateChecksum.php b/src/Generators/GenerateChecksum.php index 5cf18ac..1cb2cad 100644 --- a/src/Generators/GenerateChecksum.php +++ b/src/Generators/GenerateChecksum.php @@ -10,7 +10,7 @@ public function __invoke(): string { return md5(serialize([ Cart::items()->values()->pluck('quantity', 'hash'), - Cart::total()->amount() + Cart::total()->amount(), ])); } } diff --git a/src/Storages/CacheStorage.php b/src/Storages/CacheStorage.php index 7215794..746daeb 100644 --- a/src/Storages/CacheStorage.php +++ b/src/Storages/CacheStorage.php @@ -13,7 +13,7 @@ public function __construct($instance) $this->instance = $instance; } - public function get($key, $default = null): mixed + public function get($key, $default = null): mixed { return cache()->store(config('cart.cache_store'))->get("$this->instance.$key", $default); } diff --git a/tests/ChecknumTest.php b/tests/ChecknumTest.php index 63d686d..ff3d0e8 100644 --- a/tests/ChecknumTest.php +++ b/tests/ChecknumTest.php @@ -119,6 +119,3 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot ->and(Cart::checksum())->toBe('0ff00255271d88df533d68a60a498dff') ->and(Cart::validate('19acbc85fc3a5e4d74e08f3c78275736'))->toBeFalse(); }); - - - From ba6c7931ccb50184a5abe8b27733dfd4daab8eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Mon, 6 Nov 2023 14:58:34 +0300 Subject: [PATCH 18/36] Update Calculator.php --- src/Calculator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Calculator.php b/src/Calculator.php index 7fecd9c..58ea26b 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -17,17 +17,17 @@ public static function calculate($price, $calculations = []) { return app('pipeline') ->send([$price, []]) - ->through(collect($calculations)->reject(fn ($calculation) => $calculation === app(self::class)->skip)->toArray()) + ->through(collect($calculations)->reject(fn ($calculation) => $calculation === app(Calculator::class)->skip)->toArray()) ->thenReturn(); } public static function skip($class, $callback): mixed { - app(self::class)->skip = is_string($class) ? $class : get_class($class); + app(Calculator::class)->skip = is_string($class) ? $class : get_class($class); $value = $callback(); - app(self::class)->skip = null; + app(Calculator::class)->skip = null; return $value; } From 0fd1a33bb629bc18174a4339de672a3f9ebb5298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 9 Nov 2023 11:07:03 +0300 Subject: [PATCH 19/36] handling of meta values --- src/Cart.php | 26 ++++++++++++++++++ src/CartItem.php | 9 +++++-- src/CartItemMeta.php | 11 -------- src/Generators/GenerateSessionKey.php | 17 +++++++++++- src/MetaCollection.php | 12 +++++++++ tests/MetaTest.php | 39 +++++++++++++++++++++++++++ tests/SerializeTest.php | 10 ++++--- 7 files changed, 106 insertions(+), 18 deletions(-) delete mode 100644 src/CartItemMeta.php create mode 100644 src/MetaCollection.php create mode 100644 tests/MetaTest.php diff --git a/src/Cart.php b/src/Cart.php index e90a47b..9648ea8 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -15,6 +15,8 @@ class Cart public CartItemCollection $items; + private MetaCollection $meta; + public function __construct(readonly public CartStorage $storage) { $storage_session_key = config('cart.session_key_generator'); @@ -143,12 +145,15 @@ public function getSessionKey(): string private function save(): void { $this->putStorage('items', $this->items); + $this->putStorage('meta', $this->meta); } private function load(): void { $this->items = $this->getStorage('items', new CartItemCollection()); + $this->meta = $this->getStorage('meta', new MetaCollection); + $this->pipeline = $this->pipeline->reload($this->getStorage('pipeline')); } @@ -167,12 +172,33 @@ public function snapshot(): string return serialize($this); } + public function meta(): MetaCollection + { + return $this->meta; + } + + public function setMeta($name, $value): void + { + $this->meta->put($name, $value); + + $this->save(); + } + + public function removeMeta($name): void + { + $this->meta->forget($name); + + $this->save(); + } + public function rollback(string $string): static { $cart = unserialize($string); $this->items = $cart->items(); + $this->meta = $cart->meta(); + $this->pipeline = $this->pipeline->reload($cart->calculators()); return $this; diff --git a/src/CartItem.php b/src/CartItem.php index 7602cc7..930be4e 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -37,14 +37,19 @@ public function withOption(string $name, mixed $value, float|int $price = 0, boo $option->setWeight($weight); - $this->options->push($option); + $this->options->put($name, $option); return $this; } + public function option(string $name): CartItemOption + { + return $this->options->get($name); + } + public function withMeta(string $name, mixed $value): static { - $this->meta->push(new CartItemOption($name, $value)); + $this->meta->put($name, $value); return $this; } diff --git a/src/CartItemMeta.php b/src/CartItemMeta.php deleted file mode 100644 index 9f6cae6..0000000 --- a/src/CartItemMeta.php +++ /dev/null @@ -1,11 +0,0 @@ -id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add(new CartItem($product, 2)); + + + expect(Cart::total()->amount())->toBe(60.0); + + + Cart::setMeta('coupon', 'ABC123'); + + expect(Cart::meta()->count())->toBe(1) + ->and(Cart::meta()->get('coupon'))->toBe('ABC123'); + +}); + +it('can have items with meta data', function() { + $product = new Product(); + $product->id = 2; + $product->price = 30; + $product->weight = 4; + + Cart::add( + $cartItem = (new CartItem($product, 2))->withMeta('testMeta', 'ABC123') + ); + + expect(Cart::total()->amount())->toBe(60.0) + ->and($cartItem->meta->get('testMeta'))->toBe('ABC123'); +}); + diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index 93d8d7c..1e41c01 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -26,10 +26,10 @@ $product->weight = 1; Cart::add( - (new CartItem($product, 3)) // 40 * 3 = 120 - ->withOption('color', 'blue') + $cartItem = (new CartItem($product, 3)) // 40 * 3 = 120 + ->withOption('color', 'blue') ->withOption('size', 's', '10') // + 10 - ->withOption('size', 's', '5', true) // + 40 * 0,05 = + 2 // total 40 + 10 + 2 (56 * 3 = 156) + ->withOption('type', 'metal', 5, true) // + 40 * 0,05 = + 2 // total 40 + 10 + 2 (56 * 3 = 156) ); // Get serialized text.. @@ -47,5 +47,7 @@ expect(Cart::subtotal()->amount())->toBe(156.0) ->and(Cart::total()->amount())->toBe(156.0) ->and(Cart::items()->count())->toBe(1) - ->and(Cart::quantity())->toBe(3); + ->and(Cart::quantity())->toBe(3) + ->and($cartItem->options->count())->toBe(3) + ->and($cartItem->option('color')->value)->toBe('blue'); }); From 82946e1c2a291cdb9aa92ab7f5c47ae7196ac4b2 Mon Sep 17 00:00:00 2001 From: n1crack Date: Thu, 9 Nov 2023 09:23:03 +0000 Subject: [PATCH 20/36] Fix styling --- src/Cart.php | 2 +- src/MetaCollection.php | 3 --- tests/MetaTest.php | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Cart.php b/src/Cart.php index 9648ea8..fe400b7 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -152,7 +152,7 @@ private function load(): void { $this->items = $this->getStorage('items', new CartItemCollection()); - $this->meta = $this->getStorage('meta', new MetaCollection); + $this->meta = $this->getStorage('meta', new MetaCollection()); $this->pipeline = $this->pipeline->reload($this->getStorage('pipeline')); } diff --git a/src/MetaCollection.php b/src/MetaCollection.php index e2e12b2..23e771e 100644 --- a/src/MetaCollection.php +++ b/src/MetaCollection.php @@ -6,7 +6,4 @@ class MetaCollection extends Collection { - - - } diff --git a/tests/MetaTest.php b/tests/MetaTest.php index 99cadb8..514788d 100644 --- a/tests/MetaTest.php +++ b/tests/MetaTest.php @@ -36,4 +36,3 @@ expect(Cart::total()->amount())->toBe(60.0) ->and($cartItem->meta->get('testMeta'))->toBe('ABC123'); }); - From 745ab1b69d142338133d860bfee1079ad4c8d4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 9 Nov 2023 12:58:52 +0300 Subject: [PATCH 21/36] revert model to product --- src/CartItem.php | 10 +++++----- tests/CartTest.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CartItem.php b/src/CartItem.php index 930be4e..d4ef657 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -15,7 +15,7 @@ class CartItem implements CartItemInterface public Collection $meta; - public function __construct(public Sellable $model, public int $quantity, public bool $gift = false) + public function __construct(public Sellable $product, public int $quantity, public bool $gift = false) { $this->options = new Collection(); @@ -26,7 +26,7 @@ public function hash(): string { $options = $this->options->pluck('value')->join('-'); - return $this->hash = md5($this->model->cartItemId() . $options . $this->gift); + return $this->hash = md5($this->product->cartItemId() . $options . $this->gift); } public function withOption(string $name, mixed $value, float|int $price = 0, bool $percent = false, float|int $weight = 0): static @@ -71,12 +71,12 @@ public function decreaseQuantity(int $value): void public function weight(): float|int { - return $this->model->cartItemWeight() * $this->quantity; + return $this->product->cartItemWeight() * $this->quantity; } public function itemPrice(): Money { - return new Money($this->model->cartItemPrice()); + return new Money($this->product->cartItemPrice()); } public function optionPrice(): Money @@ -98,7 +98,7 @@ public function subtotal(): Money if (array_is_list($subtotalCalculators)) { $pipeline = $subtotalCalculators; } else { - $calculators = collect($subtotalCalculators)->filter(fn ($values) => in_array($this->model->id, $values)); + $calculators = collect($subtotalCalculators)->filter(fn ($values) => in_array($this->product->id, $values)); $pipeline = $calculators->keys()->toArray(); } diff --git a/tests/CartTest.php b/tests/CartTest.php index 068068c..8a656ab 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -267,7 +267,7 @@ $item = Cart::items()->first(); expect(Cart::item($item->hash))->toBeInstanceOf(\Ozdemir\Aurora\CartItem::class) - ->and(Cart::item($item->hash)->model->id)->toBe(3) + ->and(Cart::item($item->hash)->product->id)->toBe(3) ->and(Cart::item($item->hash)->quantity)->toBe(3) ->and(Cart::item($item->hash)->unitPrice()->amount())->toBe(30.0) ->and(Cart::item($item->hash)->weight())->toBe(3) From 11ce2faa9a1b7a9dc9f0f742a5dcff9b73bc398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Thu, 9 Nov 2023 15:20:39 +0300 Subject: [PATCH 22/36] refactor & cleanup --- config/cart.php | 32 +++++++++++++++++++++++++++----- src/Calculator.php | 14 +++++++------- src/Cart.php | 19 ++++--------------- src/CartCalculatorCollection.php | 5 ----- src/CartItem.php | 2 +- tests/CalculatorTest.php | 16 ++-------------- 6 files changed, 41 insertions(+), 47 deletions(-) diff --git a/config/cart.php b/config/cart.php index 8259afa..f9d8922 100644 --- a/config/cart.php +++ b/config/cart.php @@ -1,21 +1,43 @@ 'cart', - 'cart_class' => \Ozdemir\Aurora\Cart::class, + 'cart_class' => Cart::class, - 'storage' => \Ozdemir\Aurora\Storages\SessionStorage::class, + 'storage' => SessionStorage::class, 'cache_store' => env('CART_STORE', config('cache.default')), 'currency' => [ - 'class' => \Ozdemir\Aurora\Money::class, + 'class' => Money::class, 'precision' => env('CART_CURRENCY_PRECISION', 2), ], - 'session_key_generator' => \Ozdemir\Aurora\Generators\GenerateSessionKey::class, + 'session_key_generator' => GenerateSessionKey::class, + + 'checksum_generator' => GenerateChecksum::class, + + 'calculate_using' => [ + CartItemCalculator::SUBTOTAL->value => [ + // + ], + + CartCalculator::SUBTOTAL->value => [ + // + ], - 'checksum_generator' => \Ozdemir\Aurora\Generators\GenerateChecksum::class + CartCalculator::TOTAL->value => [ + // + ] + ] ]; diff --git a/src/Calculator.php b/src/Calculator.php index 58ea26b..c6bf3dd 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -10,24 +10,24 @@ class Calculator public function __construct() { - $this->pipeline = new CartCalculatorCollection(); + $this->pipeline = new CartCalculatorCollection(config('cart.calculate_using')); } - public static function calculate($price, $calculations = []) + public function calculate($price, $calculations = []) { - return app('pipeline') + return resolve('pipeline') ->send([$price, []]) - ->through(collect($calculations)->reject(fn ($calculation) => $calculation === app(Calculator::class)->skip)->toArray()) + ->through(collect($calculations)->reject(fn ($calculation) => $calculation === $this->skip)->toArray()) ->thenReturn(); } - public static function skip($class, $callback): mixed + public function skip($class, $callback): mixed { - app(Calculator::class)->skip = is_string($class) ? $class : get_class($class); + $this->skip = is_string($class) ? $class : get_class($class); $value = $callback(); - app(Calculator::class)->skip = null; + $this->skip = null; return $value; } diff --git a/src/Cart.php b/src/Cart.php index fe400b7..308451f 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -19,9 +19,7 @@ class Cart public function __construct(readonly public CartStorage $storage) { - $storage_session_key = config('cart.session_key_generator'); - - $this->sessionKey = (new $storage_session_key())(); + $this->sessionKey = call_user_func(new (config('cart.session_key_generator'))); $this->pipeline = app(Calculator::class)->pipeline; @@ -73,7 +71,7 @@ public function update(string $hash, int $quantity): ?CartItemInterface public function subtotal(): Money { - [$subtotal, $breakdowns] = Calculator::calculate( + [$subtotal, $breakdowns] = resolve(Calculator::class)->calculate( $this->items->subtotal(), $this->pipeline[CartCalculator::SUBTOTAL->value] ?? [] ); @@ -83,7 +81,7 @@ public function subtotal(): Money public function total(): Money { - [$total, $breakdowns] = Calculator::calculate( + [$total, $breakdowns] = resolve(Calculator::class)->calculate( $this->subtotal(), $this->pipeline[CartCalculator::TOTAL->value] ?? [] ); @@ -153,8 +151,6 @@ private function load(): void $this->items = $this->getStorage('items', new CartItemCollection()); $this->meta = $this->getStorage('meta', new MetaCollection()); - - $this->pipeline = $this->pipeline->reload($this->getStorage('pipeline')); } protected function putStorage(string $key, mixed $data): void @@ -199,8 +195,6 @@ public function rollback(string $string): static $this->meta = $cart->meta(); - $this->pipeline = $this->pipeline->reload($cart->calculators()); - return $this; } @@ -212,22 +206,17 @@ public function calculators(): CartCalculatorCollection public function calculateTotalUsing(array $pipeline): void { $this->pipeline[CartCalculator::TOTAL->value] = $pipeline; - - $this->putStorage('pipeline', $this->pipeline); } public function calculateSubtotalUsing(array $pipeline): void { $this->pipeline[CartCalculator::SUBTOTAL->value] = $pipeline; - - $this->putStorage('pipeline', $this->pipeline); } public function calculateItemSubtotalUsing(array $pipeline): void { $this->pipeline[CartItemCalculator::SUBTOTAL->value] = $pipeline; - $this->putStorage('pipeline', $this->pipeline); $this->putStorage('items', $this->items); } @@ -247,6 +236,6 @@ public function validate($clientChecksum): bool public function checksum(): string { - return (new (config('cart.checksum_generator')))(); + return call_user_func(new (config('cart.checksum_generator'))); } } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php index b7eb041..74e3ca3 100644 --- a/src/CartCalculatorCollection.php +++ b/src/CartCalculatorCollection.php @@ -6,10 +6,5 @@ class CartCalculatorCollection extends Collection { - public function reload($items = []): static - { - $this->items = $items ?? []; - return $this; - } } diff --git a/src/CartItem.php b/src/CartItem.php index d4ef657..9762c29 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -105,7 +105,7 @@ public function subtotal(): Money $subtotal = $this->unitPrice()->multiply($this->quantity); if (count($pipeline)) { - [$subtotal, $breakdowns] = Calculator::calculate( + [$subtotal, $breakdowns] = resolve(Calculator::class)->calculate( $this->unitPrice()->multiply($this->quantity), $pipeline ); diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index 92ae17b..03b91ef 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -20,13 +20,7 @@ expect(Cart::quantity())->toBe(1) ->and(Cart::count())->toBe(1) ->and(Cart::isEmpty())->toBeFalse() - ->and(Cart::calculators()->toArray())->toBe( - [ - \Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value => [ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class, - ], - ] - ) + ->and(Cart::calculators()->get(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value))->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class]) ->and(Cart::total()->amount())->toBe(40.0); }); @@ -136,13 +130,7 @@ function($payload, Closure $next) { ); expect(Cart::quantity())->toBe(1) - ->and(Cart::calculators()->toArray())->toBe( - [ - \Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value => [ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, - ], - ] - ) + ->and(Cart::calculators()->get(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value))->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class,]) ->and(Cart::subtotal()->amount())->toBe(190.0) ->and(Cart::total()->amount())->toBe(190.0) ->and( From daf3697520bcd4043df9b271224dd0d624aa167d Mon Sep 17 00:00:00 2001 From: n1crack Date: Thu, 9 Nov 2023 12:22:30 +0000 Subject: [PATCH 23/36] Fix styling --- src/CartCalculatorCollection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php index 74e3ca3..53e7c8f 100644 --- a/src/CartCalculatorCollection.php +++ b/src/CartCalculatorCollection.php @@ -6,5 +6,4 @@ class CartCalculatorCollection extends Collection { - } From dcfa3e026ced7e6b3bda2dfbeb8babb8565f37f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Sun, 12 Nov 2023 09:55:21 +0300 Subject: [PATCH 24/36] Update MetaTest.php --- tests/MetaTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/MetaTest.php b/tests/MetaTest.php index 514788d..0aafc07 100644 --- a/tests/MetaTest.php +++ b/tests/MetaTest.php @@ -21,6 +21,12 @@ expect(Cart::meta()->count())->toBe(1) ->and(Cart::meta()->get('coupon'))->toBe('ABC123'); + + Cart::removeMeta('coupon'); + + expect(Cart::meta()->count())->toBe(0); + + }); it('can have items with meta data', function() { From 8151f52f00948b29b9bbeed182a152094a08b1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 21 Nov 2023 15:27:31 +0300 Subject: [PATCH 25/36] add clone method --- src/Calculator.php | 7 ++++++- src/Cart.php | 7 ++++++- src/CartItem.php | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Calculator.php b/src/Calculator.php index c6bf3dd..c459806 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -6,13 +6,18 @@ class Calculator { public ?string $skip = null; - public CartCalculatorCollection $pipeline; + private CartCalculatorCollection $pipeline; public function __construct() { $this->pipeline = new CartCalculatorCollection(config('cart.calculate_using')); } + public function pipeline() + { + return $this->pipeline; + } + public function calculate($price, $calculations = []) { return resolve('pipeline') diff --git a/src/Cart.php b/src/Cart.php index 308451f..f05ed55 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -21,7 +21,7 @@ public function __construct(readonly public CartStorage $storage) { $this->sessionKey = call_user_func(new (config('cart.session_key_generator'))); - $this->pipeline = app(Calculator::class)->pipeline; + $this->pipeline = app(Calculator::class)->pipeline(); $this->load(); } @@ -31,6 +31,11 @@ public function make(CartStorage $storage): static return new static($storage); } + public function clone(): Cart + { + return tap($this->make($this->storage), fn (Cart $cart) => $cart->loadSession($this->getSessionKey())); + } + public function items(): CartItemCollection { return $this->items; diff --git a/src/CartItem.php b/src/CartItem.php index 9762c29..1e0f617 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -91,7 +91,7 @@ public function unitPrice(): Money public function subtotal(): Money { - $calculatorArray = app(Calculator::class)->pipeline; + $calculatorArray = app(Calculator::class)->pipeline(); $subtotalCalculators = $calculatorArray[CartItemCalculator::SUBTOTAL->value] ?? []; From 2aac46a2108ee0fd73e6ca6dd28797459bd66a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 21 Nov 2023 17:45:07 +0300 Subject: [PATCH 26/36] add MoneyInterface --- config/cart.php | 2 +- src/Cart.php | 6 ++++-- src/CartItem.php | 17 +++++++++++------ src/CartItemCollection.php | 7 +++++-- src/Contracts/MoneyInterface.php | 26 ++++++++++++++++++++++++++ src/Money.php | 17 +++++++---------- 6 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 src/Contracts/MoneyInterface.php diff --git a/config/cart.php b/config/cart.php index f9d8922..a25217c 100644 --- a/config/cart.php +++ b/config/cart.php @@ -17,7 +17,7 @@ 'cache_store' => env('CART_STORE', config('cache.default')), - 'currency' => [ + 'monetary' => [ 'class' => Money::class, 'precision' => env('CART_CURRENCY_PRECISION', 2), diff --git a/src/Cart.php b/src/Cart.php index f05ed55..d80d0a6 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -4,6 +4,7 @@ use Ozdemir\Aurora\Contracts\CartItemInterface; use Ozdemir\Aurora\Contracts\CartStorage; +use Ozdemir\Aurora\Contracts\MoneyInterface; use Ozdemir\Aurora\Enums\CartCalculator; use Ozdemir\Aurora\Enums\CartItemCalculator; @@ -74,17 +75,18 @@ public function update(string $hash, int $quantity): ?CartItemInterface }); } - public function subtotal(): Money + public function subtotal(): MoneyInterface { [$subtotal, $breakdowns] = resolve(Calculator::class)->calculate( $this->items->subtotal(), $this->pipeline[CartCalculator::SUBTOTAL->value] ?? [] ); + return $subtotal->setBreakdowns($breakdowns)->round(); } - public function total(): Money + public function total(): MoneyInterface { [$total, $breakdowns] = resolve(Calculator::class)->calculate( $this->subtotal(), diff --git a/src/CartItem.php b/src/CartItem.php index 1e0f617..3a42d7f 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; +use Ozdemir\Aurora\Contracts\MoneyInterface; use Ozdemir\Aurora\Contracts\Sellable; use Ozdemir\Aurora\Enums\CartItemCalculator; @@ -74,22 +75,26 @@ public function weight(): float|int return $this->product->cartItemWeight() * $this->quantity; } - public function itemPrice(): Money + public function itemPrice(): MoneyInterface { - return new Money($this->product->cartItemPrice()); + $money = config('cart.monetary.class'); + + return new $money($this->product->cartItemPrice()); } - public function optionPrice(): Money + public function optionPrice(): MoneyInterface { - return new Money($this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice()->amount()))); + $money = config('cart.monetary.class'); + + return new $money($this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice()->amount()))); } - public function unitPrice(): Money + public function unitPrice(): MoneyInterface { return $this->itemPrice()->add($this->optionPrice())->round(); } - public function subtotal(): Money + public function subtotal(): MoneyInterface { $calculatorArray = app(Calculator::class)->pipeline(); diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php index 2976109..f9df890 100644 --- a/src/CartItemCollection.php +++ b/src/CartItemCollection.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; +use Ozdemir\Aurora\Contracts\MoneyInterface; class CartItemCollection extends Collection { @@ -28,9 +29,11 @@ public function updateOrAdd(CartItem $cartItem): CartItem return $this->get($cartItem->hash()); } - public function subtotal(): Money + public function subtotal(): MoneyInterface { - return new Money( + $money = config('cart.monetary.class'); + + return new $money( $this->sum(fn (CartItemInterface $cartItem) => $cartItem->subtotal()->amount()) ); } diff --git a/src/Contracts/MoneyInterface.php b/src/Contracts/MoneyInterface.php new file mode 100644 index 0000000..72c6d90 --- /dev/null +++ b/src/Contracts/MoneyInterface.php @@ -0,0 +1,26 @@ +amount, $precision, $mode); return $this->newInstance($amount, $this->breakdowns); } - public function add(self $addend): static + public function add(MoneyInterface $addend): static { return $this->newInstance($this->amount + $addend->amount); } - public function subtract(self $subtrahend): static + public function subtract(MoneyInterface $subtrahend): static { return $this->newInstance($this->amount - $subtrahend->amount); } @@ -54,16 +56,11 @@ public function divide($divisor): static return $this->newInstance($this->amount / $divisor); } - private function newInstance($amount, $breakdowns = []): static + public function newInstance($amount, $breakdowns = []): static { return new self($amount, $breakdowns); } - public function isZero(): bool - { - return $this->amount == 0; - } - public function __toString() { return (string)$this->amount; From 7499943df8c32d3df22f29e6940c263ff967acb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 21 Nov 2023 18:15:59 +0300 Subject: [PATCH 27/36] Update CartServiceProvider.php --- src/CartServiceProvider.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/CartServiceProvider.php b/src/CartServiceProvider.php index 13bce8c..3fabf0c 100644 --- a/src/CartServiceProvider.php +++ b/src/CartServiceProvider.php @@ -30,21 +30,5 @@ public function register() $this->app->singleton(Calculator::class, function($app) { return new Calculator(); }); - - // \Ozdemir\Aurora\Facades\Cart::calculateUsing( - // CartCalculator::SUBTOTAL, - // [ - // Tax::class, - // ShippingClass::class - // ], - // ); - // - // \Ozdemir\Aurora\Facades\Cart::calculateUsing( - // CartCalculator::TOTAL, - // [ - // Tax::class, - // ShippingClass::class - // ], - // ); } } From 338629b3d2903a9b3cc54b08f622d0cfef0c790c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Wed, 22 Nov 2023 20:23:09 +0300 Subject: [PATCH 28/36] cleanup --- src/Generators/GenerateSessionKey.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Generators/GenerateSessionKey.php b/src/Generators/GenerateSessionKey.php index aab51d1..d96fab2 100644 --- a/src/Generators/GenerateSessionKey.php +++ b/src/Generators/GenerateSessionKey.php @@ -9,9 +9,6 @@ class GenerateSessionKey { public function __invoke(): string { - // todo: will be improved - // return Auth::check() ? 'user:' . Auth::id() : 'guest:' . uniqid(); - if (Auth::check()) { return 'user:' . Auth::id(); } From f23650366b2a7b9acdd1fc0a4dc1ddff53a58bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Fri, 24 Nov 2023 14:19:08 +0300 Subject: [PATCH 29/36] simplify & cleanup --- config/cart.php | 12 +--- src/Calculator.php | 8 +-- src/Cart.php | 55 +++++---------- src/CartItem.php | 27 ++------ src/CartItemCollection.php | 2 + src/Contracts/MoneyInterface.php | 4 -- src/Enums/CartCalculator.php | 10 --- src/Enums/CartItemCalculator.php | 10 --- src/Money.php | 10 --- src/Traits/RoundingTrait.php | 13 ++++ tests/CalculatorTest.php | 115 +------------------------------ 11 files changed, 44 insertions(+), 222 deletions(-) delete mode 100644 src/Enums/CartCalculator.php delete mode 100644 src/Enums/CartItemCalculator.php create mode 100644 src/Traits/RoundingTrait.php diff --git a/config/cart.php b/config/cart.php index a25217c..e36b9bc 100644 --- a/config/cart.php +++ b/config/cart.php @@ -28,16 +28,6 @@ 'checksum_generator' => GenerateChecksum::class, 'calculate_using' => [ - CartItemCalculator::SUBTOTAL->value => [ - // - ], - - CartCalculator::SUBTOTAL->value => [ - // - ], - - CartCalculator::TOTAL->value => [ - // - ] + // ] ]; diff --git a/src/Calculator.php b/src/Calculator.php index c459806..b77aa74 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -6,16 +6,16 @@ class Calculator { public ?string $skip = null; - private CartCalculatorCollection $pipeline; + private CartCalculatorCollection $calculators; public function __construct() { - $this->pipeline = new CartCalculatorCollection(config('cart.calculate_using')); + $this->calculators = new CartCalculatorCollection(config('cart.calculate_using')); } - public function pipeline() + public function calculators(): CartCalculatorCollection { - return $this->pipeline; + return $this->calculators; } public function calculate($price, $calculations = []) diff --git a/src/Cart.php b/src/Cart.php index d80d0a6..7ddb0e0 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -5,14 +5,15 @@ use Ozdemir\Aurora\Contracts\CartItemInterface; use Ozdemir\Aurora\Contracts\CartStorage; use Ozdemir\Aurora\Contracts\MoneyInterface; -use Ozdemir\Aurora\Enums\CartCalculator; -use Ozdemir\Aurora\Enums\CartItemCalculator; +use Ozdemir\Aurora\Traits\RoundingTrait; class Cart { + use RoundingTrait; + private string $sessionKey; - private CartCalculatorCollection $pipeline; + private CartCalculatorCollection $calculators; public CartItemCollection $items; @@ -22,7 +23,7 @@ public function __construct(readonly public CartStorage $storage) { $this->sessionKey = call_user_func(new (config('cart.session_key_generator'))); - $this->pipeline = app(Calculator::class)->pipeline(); + $this->calculators = app(Calculator::class)->calculators(); $this->load(); } @@ -75,22 +76,16 @@ public function update(string $hash, int $quantity): ?CartItemInterface }); } - public function subtotal(): MoneyInterface + public function subtotal() { - [$subtotal, $breakdowns] = resolve(Calculator::class)->calculate( - $this->items->subtotal(), - $this->pipeline[CartCalculator::SUBTOTAL->value] ?? [] - ); - - - return $subtotal->setBreakdowns($breakdowns)->round(); + return $this->items->subtotal()->round(); } - public function total(): MoneyInterface + public function total() { [$total, $breakdowns] = resolve(Calculator::class)->calculate( $this->subtotal(), - $this->pipeline[CartCalculator::TOTAL->value] ?? [] + $this->calculators ?? new CartCalculatorCollection() ); return $total->setBreakdowns($breakdowns)->round(); @@ -170,11 +165,6 @@ protected function getStorage(string $key, $default = null): mixed return $this->storage->get($this->getSessionKey() . '.' . $key, $default); } - public function snapshot(): string - { - return serialize($this); - } - public function meta(): MetaCollection { return $this->meta; @@ -194,6 +184,11 @@ public function removeMeta($name): void $this->save(); } + public function snapshot(): string + { + return serialize($this); + } + public function rollback(string $string): static { $cart = unserialize($string); @@ -207,31 +202,19 @@ public function rollback(string $string): static public function calculators(): CartCalculatorCollection { - return $this->pipeline; - } - - public function calculateTotalUsing(array $pipeline): void - { - $this->pipeline[CartCalculator::TOTAL->value] = $pipeline; + return $this->calculators; } - public function calculateSubtotalUsing(array $pipeline): void + public function calculateTotalUsing(array $calculators): void { - $this->pipeline[CartCalculator::SUBTOTAL->value] = $pipeline; - } - - public function calculateItemSubtotalUsing(array $pipeline): void - { - $this->pipeline[CartItemCalculator::SUBTOTAL->value] = $pipeline; - - $this->putStorage('items', $this->items); + $this->calculators = new CartCalculatorCollection($calculators); } public function instance(): array { return [ - 'subtotal' => $this->subtotal(), - 'total' => $this->total(), + // subtotal' => $this->subtotal(), + // total' => $this->total(), // todo.. breakdowns etc.. ]; } diff --git a/src/CartItem.php b/src/CartItem.php index 3a42d7f..90dddc5 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -6,10 +6,12 @@ use Ozdemir\Aurora\Contracts\CartItemInterface; use Ozdemir\Aurora\Contracts\MoneyInterface; use Ozdemir\Aurora\Contracts\Sellable; -use Ozdemir\Aurora\Enums\CartItemCalculator; +use Ozdemir\Aurora\Traits\RoundingTrait; class CartItem implements CartItemInterface { + use RoundingTrait; + public string $hash; public Collection $options; @@ -96,28 +98,7 @@ public function unitPrice(): MoneyInterface public function subtotal(): MoneyInterface { - $calculatorArray = app(Calculator::class)->pipeline(); - - $subtotalCalculators = $calculatorArray[CartItemCalculator::SUBTOTAL->value] ?? []; - - if (array_is_list($subtotalCalculators)) { - $pipeline = $subtotalCalculators; - } else { - $calculators = collect($subtotalCalculators)->filter(fn ($values) => in_array($this->product->id, $values)); - $pipeline = $calculators->keys()->toArray(); - } - - $subtotal = $this->unitPrice()->multiply($this->quantity); - - if (count($pipeline)) { - [$subtotal, $breakdowns] = resolve(Calculator::class)->calculate( - $this->unitPrice()->multiply($this->quantity), - $pipeline - ); - - return $subtotal->setBreakdowns($breakdowns)->round(); - } - return $subtotal->round(); + return $this->unitPrice()->multiply($this->quantity); } } diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php index f9df890..ef54e88 100644 --- a/src/CartItemCollection.php +++ b/src/CartItemCollection.php @@ -5,9 +5,11 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; use Ozdemir\Aurora\Contracts\MoneyInterface; +use Ozdemir\Aurora\Traits\RoundingTrait; class CartItemCollection extends Collection { + use RoundingTrait; /** * @param string $key */ diff --git a/src/Contracts/MoneyInterface.php b/src/Contracts/MoneyInterface.php index 72c6d90..27f4ebb 100644 --- a/src/Contracts/MoneyInterface.php +++ b/src/Contracts/MoneyInterface.php @@ -18,9 +18,5 @@ public function subtract(MoneyInterface $subtrahend): static; public function multiply($multiplier): static; - public function divide($divisor): static; - public function newInstance($amount, $breakdowns = []): static; - - public function __toString(); } diff --git a/src/Enums/CartCalculator.php b/src/Enums/CartCalculator.php deleted file mode 100644 index c121bc6..0000000 --- a/src/Enums/CartCalculator.php +++ /dev/null @@ -1,10 +0,0 @@ -newInstance($this->amount * $multiplier); } - public function divide($divisor): static - { - return $this->newInstance($this->amount / $divisor); - } - public function newInstance($amount, $breakdowns = []): static { return new self($amount, $breakdowns); } - - public function __toString() - { - return (string)$this->amount; - } } diff --git a/src/Traits/RoundingTrait.php b/src/Traits/RoundingTrait.php new file mode 100644 index 0000000..c5b2f9d --- /dev/null +++ b/src/Traits/RoundingTrait.php @@ -0,0 +1,13 @@ +toBe(1) ->and(Cart::count())->toBe(1) ->and(Cart::isEmpty())->toBeFalse() - ->and(Cart::calculators()->get(\Ozdemir\Aurora\Enums\CartCalculator::TOTAL->value))->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class]) + ->and(Cart::calculators()->toArray())->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class]) ->and(Cart::total()->amount())->toBe(40.0); }); @@ -113,116 +113,3 @@ function($payload, Closure $next) { expect(Cart::total()->amount())->toBe(115.0) ->and(Cart::total()->breakdowns())->toHaveCount(2); }); - - -it('can have calculators on cart subtotal', function() { - - Cart::calculateSubtotalUsing([ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // - 5% - ]); - - $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product->id = 3; - $product->price = 200; - - Cart::add( - new CartItem($product, quantity: 1), - ); - - expect(Cart::quantity())->toBe(1) - ->and(Cart::calculators()->get(\Ozdemir\Aurora\Enums\CartCalculator::SUBTOTAL->value))->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class,]) - ->and(Cart::subtotal()->amount())->toBe(190.0) - ->and(Cart::total()->amount())->toBe(190.0) - ->and( - json_encode(Cart::subtotal()->breakdowns()) - ) - ->toBe( - json_encode([ - ['label' => 'Discount', 'value' => new Money(-10)], - ]) - ); -}); - - -it('can have calculators on sellable item subtotal', function() { - - Cart::calculateItemSubtotalUsing([ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class => [3, 6], // for the items that has these ids run the discount -5% - ]); - - $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product->id = 3; - $product->price = 200; - - $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product2->id = 4; - $product2->price = 50; - - $product3 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product3->id = 6; - $product3->price = 100; - - Cart::add( - new CartItem($product, quantity: 1), // 200 * 0,95 = 190 - new CartItem($product2, quantity: 3), // 150 = 150 - new CartItem($product3, quantity: 1), // 100 * 0,95 = 95 - ); - - expect(Cart::quantity())->toBe(5) - ->and(Cart::subtotal()->amount())->toBe(435.0) - ->and(Cart::total()->amount())->toBe(435.0); -}); - -it('can have default calculators on subtotals of every sellable items', function() { - Cart::calculateItemSubtotalUsing([ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // for the items that has these ids run the discount -5% - ]); - - $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product->id = 3; - $product->price = 200; - - $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product2->id = 4; - $product2->price = 50; - - Cart::add( - new CartItem($product, quantity: 1), // 200 * 0,95 = 190 - new CartItem($product2, quantity: 3) // 150 * 0,95 = 142,5 - ); - - expect(Cart::quantity())->toBe(4) - ->and(Cart::subtotal()->amount())->toBe(332.5) - ->and(Cart::total()->amount())->toBe(332.5); -}); - -it('can have inline calculators on sellable item subtotal', function() { - Cart::calculateItemSubtotalUsing([ - \Ozdemir\Aurora\Tests\Stubs\Calculators\Discount::class, // for the items that has these ids run the discount -5% - function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. - [$price, $breakdowns] = $payload; - $price = $price->add(new Money(0.01)); - $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '0.01$']; - - return $next([$price, $breakdowns]); - }, - ]); - - $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product->id = 3; - $product->price = 200; - - $product2 = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); - $product2->id = 4; - $product2->price = 50; - - Cart::add( - new CartItem($product, quantity: 1), // 200 * 0,95 = 190 + 0.01 - new CartItem($product2, quantity: 3) // 150 * 0,95 = 142,5 + 0.01 - ); - - expect(Cart::quantity())->toBe(4) - ->and(Cart::subtotal()->amount())->toBe(332.52) - ->and(Cart::items()->first()->subtotal()->amount())->toBe(190.01) - ->and(Cart::items()->first()->subtotal()->breakdowns()[1])->toBe(['label' => 'Custom Shipping', 'value' => '0.01$']); -}); From f04000c946aaa7a8a90f0a7f43f245b6cabf3cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Fri, 24 Nov 2023 18:53:50 +0300 Subject: [PATCH 30/36] refactor code and cleanup --- config/cart.php | 3 -- src/CalculationResult.php | 25 ++++++++++++ src/Calculator.php | 8 ++-- src/Cart.php | 52 ++++++++++++++++++------- src/CartCalculatorCollection.php | 9 ----- src/CartItem.php | 22 ++++------- src/CartItemCollection.php | 10 ++--- src/Contracts/MoneyInterface.php | 22 ----------- src/Generators/GenerateChecksum.php | 4 +- src/Money.php | 58 ---------------------------- tests/CalculatorTest.php | 30 +++++++------- tests/CartTest.php | 24 ++++++------ tests/ChecknumTest.php | 17 ++++---- tests/MetaTest.php | 5 +-- tests/SerializeTest.php | 4 +- tests/Stubs/Calculators/Discount.php | 8 ++-- tests/Stubs/Calculators/Shipping.php | 6 +-- tests/Stubs/Calculators/Tax.php | 6 +-- 18 files changed, 124 insertions(+), 189 deletions(-) create mode 100644 src/CalculationResult.php delete mode 100644 src/CartCalculatorCollection.php delete mode 100644 src/Contracts/MoneyInterface.php delete mode 100644 src/Money.php diff --git a/config/cart.php b/config/cart.php index e36b9bc..5fadc70 100644 --- a/config/cart.php +++ b/config/cart.php @@ -1,11 +1,8 @@ total = $total; + $this->breakdowns = $breakdowns; + } + + public function total(): float + { + return $this->total; + } + + public function breakdowns(): mixed + { + return $this->breakdowns; + } +} diff --git a/src/Calculator.php b/src/Calculator.php index b77aa74..6a4b703 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -2,18 +2,20 @@ namespace Ozdemir\Aurora; +use Illuminate\Support\Collection; + class Calculator { public ?string $skip = null; - private CartCalculatorCollection $calculators; + private Collection $calculators; public function __construct() { - $this->calculators = new CartCalculatorCollection(config('cart.calculate_using')); + $this->calculators = new Collection(config('cart.calculate_using')); } - public function calculators(): CartCalculatorCollection + public function calculators(): Collection { return $this->calculators; } diff --git a/src/Cart.php b/src/Cart.php index 7ddb0e0..5d57de9 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -2,9 +2,9 @@ namespace Ozdemir\Aurora; +use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; use Ozdemir\Aurora\Contracts\CartStorage; -use Ozdemir\Aurora\Contracts\MoneyInterface; use Ozdemir\Aurora\Traits\RoundingTrait; class Cart @@ -13,12 +13,14 @@ class Cart private string $sessionKey; - private CartCalculatorCollection $calculators; + private Collection $calculators; public CartItemCollection $items; private MetaCollection $meta; + private string $calculationChecksum = ''; + public function __construct(readonly public CartStorage $storage) { $this->sessionKey = call_user_func(new (config('cart.session_key_generator'))); @@ -76,19 +78,41 @@ public function update(string $hash, int $quantity): ?CartItemInterface }); } - public function subtotal() + public function subtotal(): float { - return $this->items->subtotal()->round(); + return $this->round($this->items->subtotal()); } - public function total() + public function calculate(): CalculationResult { - [$total, $breakdowns] = resolve(Calculator::class)->calculate( - $this->subtotal(), - $this->calculators ?? new CartCalculatorCollection() - ); + $checksum = md5(($subtotal = $this->subtotal()) . $this->calculators ?? new Collection()); + + if ($this->calculationChecksum !== $checksum || $this->calculators->contains(fn ($item) => $item instanceof \Closure)) { + [$total, $breakdowns] = resolve(Calculator::class)->calculate( + $subtotal, + $this->calculators ?? new Collection() + ); + $this->calculationChecksum = $checksum; + $this->results = [$total, $breakdowns]; + } else { + [$total, $breakdowns] = $this->results; + } + +// [$total, $breakdowns] = resolve(Calculator::class)->calculate( +// $this->subtotal(), +// $this->calculators ?? new Collection() +// ); + return new CalculationResult($total, $breakdowns); + } - return $total->setBreakdowns($breakdowns)->round(); + public function total(): float + { + return $this->calculate()->total(); + } + + public function breakdowns() + { + return $this->calculate()->breakdowns(); } public function weight(): float|int @@ -200,14 +224,14 @@ public function rollback(string $string): static return $this; } - public function calculators(): CartCalculatorCollection + public function calculators(): Collection { return $this->calculators; } public function calculateTotalUsing(array $calculators): void { - $this->calculators = new CartCalculatorCollection($calculators); + $this->calculators = new Collection($calculators); } public function instance(): array @@ -224,8 +248,8 @@ public function validate($clientChecksum): bool return $this->checksum() === $clientChecksum; } - public function checksum(): string + public function checksum($withTotal = true): string { - return call_user_func(new (config('cart.checksum_generator'))); + return call_user_func(new (config('cart.checksum_generator')), $withTotal); } } diff --git a/src/CartCalculatorCollection.php b/src/CartCalculatorCollection.php deleted file mode 100644 index 53e7c8f..0000000 --- a/src/CartCalculatorCollection.php +++ /dev/null @@ -1,9 +0,0 @@ -product->cartItemWeight() * $this->quantity; } - public function itemPrice(): MoneyInterface + public function itemPrice(): float|int { - $money = config('cart.monetary.class'); - - return new $money($this->product->cartItemPrice()); + return $this->product->cartItemPrice(); } - public function optionPrice(): MoneyInterface + public function optionPrice(): float|int { - $money = config('cart.monetary.class'); - - return new $money($this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice()->amount()))); + return $this->round($this->options->sum(fn (CartItemOption $option) => $option->getPrice($this->itemPrice()))); } - public function unitPrice(): MoneyInterface + public function unitPrice(): float|int { - return $this->itemPrice()->add($this->optionPrice())->round(); + return $this->round($this->itemPrice() + $this->optionPrice()); } - public function subtotal(): MoneyInterface + public function subtotal(): float|int { - - return $this->unitPrice()->multiply($this->quantity); + return $this->unitPrice() * $this->quantity; } } diff --git a/src/CartItemCollection.php b/src/CartItemCollection.php index ef54e88..f60fe71 100644 --- a/src/CartItemCollection.php +++ b/src/CartItemCollection.php @@ -4,12 +4,12 @@ use Illuminate\Support\Collection; use Ozdemir\Aurora\Contracts\CartItemInterface; -use Ozdemir\Aurora\Contracts\MoneyInterface; use Ozdemir\Aurora\Traits\RoundingTrait; class CartItemCollection extends Collection { use RoundingTrait; + /** * @param string $key */ @@ -31,12 +31,8 @@ public function updateOrAdd(CartItem $cartItem): CartItem return $this->get($cartItem->hash()); } - public function subtotal(): MoneyInterface + public function subtotal(): float|int { - $money = config('cart.monetary.class'); - - return new $money( - $this->sum(fn (CartItemInterface $cartItem) => $cartItem->subtotal()->amount()) - ); + return $this->sum(fn (CartItemInterface $cartItem) => $cartItem->subtotal()); } } diff --git a/src/Contracts/MoneyInterface.php b/src/Contracts/MoneyInterface.php deleted file mode 100644 index 27f4ebb..0000000 --- a/src/Contracts/MoneyInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -values()->pluck('quantity', 'hash'), - Cart::total()->amount(), + Cart::total(), ])); } } diff --git a/src/Money.php b/src/Money.php deleted file mode 100644 index e2559f9..0000000 --- a/src/Money.php +++ /dev/null @@ -1,58 +0,0 @@ -amount; - } - - public function breakdowns(): array - { - return $this->breakdowns; - } - - public function setBreakdowns($breakdowns): static - { - $this->breakdowns = $breakdowns; - - return new self($this->amount, $breakdowns); - } - - public function round($precision = null, $mode = PHP_ROUND_HALF_UP): static - { - $precision ??= config('cart.monetary.precision', 2); - - $amount = round($this->amount, $precision, $mode); - - return $this->newInstance($amount, $this->breakdowns); - } - - public function add(MoneyInterface $addend): static - { - return $this->newInstance($this->amount + $addend->amount); - } - - public function subtract(MoneyInterface $subtrahend): static - { - return $this->newInstance($this->amount - $subtrahend->amount); - } - - public function multiply($multiplier): static - { - return $this->newInstance($this->amount * $multiplier); - } - - public function newInstance($amount, $breakdowns = []): static - { - return new self($amount, $breakdowns); - } -} diff --git a/tests/CalculatorTest.php b/tests/CalculatorTest.php index d445554..cfc55b6 100644 --- a/tests/CalculatorTest.php +++ b/tests/CalculatorTest.php @@ -2,7 +2,6 @@ use Ozdemir\Aurora\CartItem; use Ozdemir\Aurora\Facades\Cart; -use Ozdemir\Aurora\Money; it('can use calculators', function() { Cart::calculateTotalUsing([ @@ -21,7 +20,7 @@ ->and(Cart::count())->toBe(1) ->and(Cart::isEmpty())->toBeFalse() ->and(Cart::calculators()->toArray())->toBe([\Ozdemir\Aurora\Tests\Stubs\Calculators\Shipping::class]) - ->and(Cart::total()->amount())->toBe(40.0); + ->and(Cart::total())->toBe(40.0); }); @@ -41,7 +40,7 @@ expect(Cart::quantity())->toBe(1) ->and(Cart::isEmpty())->toBeFalse() - ->and(Cart::total()->amount())->toBe(55.0); + ->and(Cart::total())->toBe(55.0); }); it('can have breakdowns', function() { @@ -59,41 +58,38 @@ new CartItem($product, quantity: 2), ); - expect(Cart::total()->amount())->toBe(115.0) - ->and(Cart::total()->breakdowns())->toHaveCount(2) + expect(Cart::total())->toBe(115.0) + ->and(Cart::breakdowns())->toHaveCount(2) ->and( - json_encode(Cart::total()->breakdowns()) + json_encode(Cart::breakdowns()) ) ->toBe( json_encode([ - ['label' => 'Shipping', 'value' => new Money(10)], - ['label' => 'Tax', 'value' => new Money(15)], + ['label' => 'Shipping', 'value' => 10], + ['label' => 'Tax', 'value' => 15], ]) ); }); - it('can have inline calculators', function() { Cart::calculateTotalUsing([ function($payload, Closure $next) { - /* @var Money $price */ [$price, $breakdowns] = $payload; - $shippingCost = new Money(10); + $shippingCost = 10; - $price = $price->add($shippingCost); + $price = $price + $shippingCost; $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; return $next([$price, $breakdowns]); }, function($payload, Closure $next) { - /* @var Money $price */ [$price, $breakdowns] = $payload; - $taxCost = new Money(15); + $taxCost = 15; - $price = $price->add($taxCost); + $price = $price + $taxCost; $breakdowns[] = ['label' => 'Shipping', 'value' => $taxCost]; @@ -110,6 +106,6 @@ function($payload, Closure $next) { new CartItem($product, quantity: 2), ); - expect(Cart::total()->amount())->toBe(115.0) - ->and(Cart::total()->breakdowns())->toHaveCount(2); + expect(Cart::total())->toBe(115.0) + ->and(Cart::breakdowns())->toHaveCount(2); }); diff --git a/tests/CartTest.php b/tests/CartTest.php index 8a656ab..bfc70e5 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -39,7 +39,7 @@ ); expect(Cart::quantity())->toBe(1) - ->and(Cart::total()->amount())->toBe(30.0) + ->and(Cart::total())->toBe(30.0) ->and(Cart::isEmpty())->toBeFalse(); }); @@ -141,8 +141,8 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total()->amount())->toBe(530.0) - ->and(Cart::subtotal()->amount())->toBe(530.0) + ->and(Cart::total())->toBe(530.0) + ->and(Cart::subtotal())->toBe(530.0) ->and(Cart::weight())->toBe(10); }); @@ -162,7 +162,7 @@ expect(Cart::isEmpty())->toBeTrue() ->and(Cart::items())->toHaveCount(0) - ->and(Cart::total()->amount())->toBe(0.0) + ->and(Cart::total())->toBe(0.0) ->and(Cart::weight())->toBe(0); }); @@ -182,7 +182,7 @@ Cart::add(new CartItem($product2, 1)); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total()->amount())->toBe(530.0) + ->and(Cart::total())->toBe(530.0) ->and(Cart::quantity())->toBe(3) ->and(Cart::weight())->toBe(10); @@ -191,8 +191,8 @@ Cart::remove($cartItem->hash()); expect(Cart::items())->toHaveCount(1) - ->and(Cart::total()->amount())->toBe(60.0) - ->and(Cart::subtotal()->amount())->toBe(60.0) + ->and(Cart::total())->toBe(60.0) + ->and(Cart::subtotal())->toBe(60.0) ->and(Cart::quantity())->toBe(2) ->and(Cart::weight())->toBe(2); }); @@ -214,7 +214,7 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total()->amount())->toBe(530.0) + ->and(Cart::total())->toBe(530.0) ->and(Cart::quantity())->toBe(3) ->and(Cart::weight())->toBe(10); @@ -229,7 +229,7 @@ ); expect(Cart::items())->toHaveCount(2) - ->and(Cart::total()->amount())->toBe(1510.0) + ->and(Cart::total())->toBe(1510.0) ->and(Cart::quantity())->toBe(12) ->and(Cart::weight())->toBe(20); }); @@ -269,7 +269,7 @@ expect(Cart::item($item->hash))->toBeInstanceOf(\Ozdemir\Aurora\CartItem::class) ->and(Cart::item($item->hash)->product->id)->toBe(3) ->and(Cart::item($item->hash)->quantity)->toBe(3) - ->and(Cart::item($item->hash)->unitPrice()->amount())->toBe(30.0) + ->and(Cart::item($item->hash)->unitPrice())->toBe(30.0) ->and(Cart::item($item->hash)->weight())->toBe(3) ->and(Cart::item($item->hash)->options)->toBeInstanceOf(Collection::class) ->and(Cart::item($item->hash)->options)->toHaveCount(2) @@ -343,7 +343,7 @@ ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) - ->and(Cart::total()->amount())->toBe(0.0) + ->and(Cart::total())->toBe(0.0) ->and(Cart::getInstanceKey())->toBe('cart'); }); @@ -383,7 +383,7 @@ ->and(Cart::items())->toHaveCount(0) ->and(Cart::quantity())->toBe(0) ->and(Cart::weight())->toBe(0) - ->and(Cart::total()->amount())->toBe(0.0) + ->and(Cart::total())->toBe(0.0) ->and(Cart::getInstanceKey())->toBe('cart'); Cart::loadSession($oldSession); diff --git a/tests/ChecknumTest.php b/tests/ChecknumTest.php index ff3d0e8..71392f9 100644 --- a/tests/ChecknumTest.php +++ b/tests/ChecknumTest.php @@ -3,7 +3,6 @@ use Ozdemir\Aurora\CartItem; use Ozdemir\Aurora\Facades\Cart; -use Ozdemir\Aurora\Money; it('has checknum and can be validated', function() { $product = new \Ozdemir\Aurora\Tests\Stubs\Models\Product(); @@ -42,12 +41,10 @@ expect(Cart::quantity())->toBe(3) ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); - Cart::clear(); expect(Cart::quantity())->toBe(0) ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); - }); @@ -60,7 +57,7 @@ Cart::add(new CartItem($product, 2)); expect(Cart::quantity())->toBe(2) - ->and(Cart::total()->amount())->toBe(60.0) + ->and(Cart::total())->toBe(60.0) ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); @@ -70,7 +67,7 @@ ]); expect(Cart::quantity())->toBe(2) - ->and(Cart::total()->amount())->toBe(85.0) + ->and(Cart::total())->toBe(85.0) ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); }); @@ -85,14 +82,14 @@ Cart::add(new CartItem($product, 2)); expect(Cart::quantity())->toBe(2) - ->and(Cart::total()->amount())->toBe(60.0) + ->and(Cart::total())->toBe(60.0) ->and(Cart::checksum())->toBe('5b520de298c71f3deaa3544809e0be2e') ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeTrue(); Cart::calculateTotalUsing([ function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. [$price, $breakdowns] = $payload; - $price = $price->add(new Money(10)); + $price = $price + 10; $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '10$']; return $next([$price, $breakdowns]); @@ -100,14 +97,14 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot ]); expect(Cart::quantity())->toBe(2) - ->and(Cart::total()->amount())->toBe(70.0) + ->and(Cart::total())->toBe(70.0) ->and(Cart::checksum())->toBe('19acbc85fc3a5e4d74e08f3c78275736') ->and(Cart::validate('5b520de298c71f3deaa3544809e0be2e'))->toBeFalse(); Cart::calculateTotalUsing([ function($payload, Closure $next) { // simply add + 0.01$ for every items subtotal value. [$price, $breakdowns] = $payload; - $price = $price->add(new Money(11)); + $price = $price + 11; $breakdowns[] = ['label' => 'Custom Shipping', 'value' => '11$']; return $next([$price, $breakdowns]); @@ -115,7 +112,7 @@ function($payload, Closure $next) { // simply add + 0.01$ for every items subtot ]); expect(Cart::quantity())->toBe(2) - ->and(Cart::total()->amount())->toBe(71.0) + ->and(Cart::total())->toBe(71.0) ->and(Cart::checksum())->toBe('0ff00255271d88df533d68a60a498dff') ->and(Cart::validate('19acbc85fc3a5e4d74e08f3c78275736'))->toBeFalse(); }); diff --git a/tests/MetaTest.php b/tests/MetaTest.php index 0aafc07..8d130ee 100644 --- a/tests/MetaTest.php +++ b/tests/MetaTest.php @@ -13,8 +13,7 @@ Cart::add(new CartItem($product, 2)); - expect(Cart::total()->amount())->toBe(60.0); - + expect(Cart::total())->toBe(60.0); Cart::setMeta('coupon', 'ABC123'); @@ -39,6 +38,6 @@ $cartItem = (new CartItem($product, 2))->withMeta('testMeta', 'ABC123') ); - expect(Cart::total()->amount())->toBe(60.0) + expect(Cart::total())->toBe(60.0) ->and($cartItem->meta->get('testMeta'))->toBe('ABC123'); }); diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index 1e41c01..59910d7 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -44,8 +44,8 @@ // restore the cart from snapshot Cart::rollback($snapshot); - expect(Cart::subtotal()->amount())->toBe(156.0) - ->and(Cart::total()->amount())->toBe(156.0) + expect(Cart::subtotal())->toBe(156.0) + ->and(Cart::total())->toBe(156.0) ->and(Cart::items()->count())->toBe(1) ->and(Cart::quantity())->toBe(3) ->and($cartItem->options->count())->toBe(3) diff --git a/tests/Stubs/Calculators/Discount.php b/tests/Stubs/Calculators/Discount.php index 676e454..9376b21 100644 --- a/tests/Stubs/Calculators/Discount.php +++ b/tests/Stubs/Calculators/Discount.php @@ -3,7 +3,6 @@ namespace Ozdemir\Aurora\Tests\Stubs\Calculators; use Closure; -use Ozdemir\Aurora\Money; /* @noinspection */ @@ -11,20 +10,19 @@ class Discount { public function handle($payload, Closure $next) { - /* @var Money $price */ [$price, $breakdowns] = $payload; // $total = \Ozdemir\Aurora\Calculator::skip($this, function() { - // return \Ozdemir\Aurora\Facades\Cart::total()->amount(); + // return \Ozdemir\Aurora\Facades\Cart::total(); // }); // // dump($total); - $discountPrice = new Money($price->multiply(5 / 100)->amount()); + $discountPrice = $price * 5 / 100; $price = $price->subtract($discountPrice); - $breakdowns[] = ['label' => 'Discount', 'value' => $discountPrice->multiply(-1)]; + $breakdowns[] = ['label' => 'Discount', 'value' => -1 * $discountPrice]; return $next([$price, $breakdowns]); } diff --git a/tests/Stubs/Calculators/Shipping.php b/tests/Stubs/Calculators/Shipping.php index 38b5420..d43c7bb 100644 --- a/tests/Stubs/Calculators/Shipping.php +++ b/tests/Stubs/Calculators/Shipping.php @@ -3,7 +3,6 @@ namespace Ozdemir\Aurora\Tests\Stubs\Calculators; use Closure; -use Ozdemir\Aurora\Money; /* @noinspection */ @@ -11,12 +10,11 @@ class Shipping { public function handle($payload, Closure $next) { - /* @var Money $price */ [$price, $breakdowns] = $payload; - $shippingCost = new Money(10); + $shippingCost = 10; - $price = $price->add($shippingCost); + $price = $price + $shippingCost; $breakdowns[] = ['label' => 'Shipping', 'value' => $shippingCost]; diff --git a/tests/Stubs/Calculators/Tax.php b/tests/Stubs/Calculators/Tax.php index d37d07b..a916df2 100644 --- a/tests/Stubs/Calculators/Tax.php +++ b/tests/Stubs/Calculators/Tax.php @@ -3,7 +3,6 @@ namespace Ozdemir\Aurora\Tests\Stubs\Calculators; use Closure; -use Ozdemir\Aurora\Money; /* @noinspection */ @@ -11,12 +10,11 @@ class Tax { public function handle($payload, Closure $next) { - /* @var Money $price */ [$price, $breakdowns] = $payload; - $taxPrice = new Money(15); + $taxPrice = 15; - $price = $price->add($taxPrice); + $price = $price + $taxPrice; $breakdowns[] = ['label' => 'Tax', 'value' => $taxPrice]; From 11f2a68fefdd07dfaee9e49ba037b79dc1fb8198 Mon Sep 17 00:00:00 2001 From: n1crack Date: Fri, 24 Nov 2023 15:54:08 +0000 Subject: [PATCH 31/36] Fix styling --- src/Cart.php | 8 ++++---- src/Generators/GenerateChecksum.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cart.php b/src/Cart.php index 5d57de9..5a04b6c 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -98,10 +98,10 @@ public function calculate(): CalculationResult [$total, $breakdowns] = $this->results; } -// [$total, $breakdowns] = resolve(Calculator::class)->calculate( -// $this->subtotal(), -// $this->calculators ?? new Collection() -// ); + // [$total, $breakdowns] = resolve(Calculator::class)->calculate( + // $this->subtotal(), + // $this->calculators ?? new Collection() + // ); return new CalculationResult($total, $breakdowns); } diff --git a/src/Generators/GenerateChecksum.php b/src/Generators/GenerateChecksum.php index 6f3f89b..70d77ed 100644 --- a/src/Generators/GenerateChecksum.php +++ b/src/Generators/GenerateChecksum.php @@ -6,7 +6,7 @@ class GenerateChecksum { - public function __invoke($withTotal= true): string + public function __invoke($withTotal = true): string { return md5(serialize([ Cart::items()->values()->pluck('quantity', 'hash'), From 96491ba4a53648d36f6212b6d9a8684b3ec502cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Sat, 25 Nov 2023 15:29:02 +0300 Subject: [PATCH 32/36] Update Cart.php --- src/Cart.php | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Cart.php b/src/Cart.php index 5a04b6c..a62b0c5 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -19,8 +19,6 @@ class Cart private MetaCollection $meta; - private string $calculationChecksum = ''; - public function __construct(readonly public CartStorage $storage) { $this->sessionKey = call_user_func(new (config('cart.session_key_generator'))); @@ -85,23 +83,10 @@ public function subtotal(): float public function calculate(): CalculationResult { - $checksum = md5(($subtotal = $this->subtotal()) . $this->calculators ?? new Collection()); - - if ($this->calculationChecksum !== $checksum || $this->calculators->contains(fn ($item) => $item instanceof \Closure)) { - [$total, $breakdowns] = resolve(Calculator::class)->calculate( - $subtotal, - $this->calculators ?? new Collection() - ); - $this->calculationChecksum = $checksum; - $this->results = [$total, $breakdowns]; - } else { - [$total, $breakdowns] = $this->results; - } - - // [$total, $breakdowns] = resolve(Calculator::class)->calculate( - // $this->subtotal(), - // $this->calculators ?? new Collection() - // ); + [$total, $breakdowns] = resolve(Calculator::class)->calculate( + $this->subtotal(), + $this->calculators ?? new Collection() + ); return new CalculationResult($total, $breakdowns); } From a65025e99799b2e662c5297a190eded88a71cc31 Mon Sep 17 00:00:00 2001 From: n1crack Date: Tue, 28 Nov 2023 08:16:00 +0000 Subject: [PATCH 33/36] Fix styling --- src/Cart.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cart.php b/src/Cart.php index a62b0c5..dfc1d54 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -87,6 +87,7 @@ public function calculate(): CalculationResult $this->subtotal(), $this->calculators ?? new Collection() ); + return new CalculationResult($total, $breakdowns); } From 007cffc225abb3dddcb17bb382cc91764d5eee96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 28 Nov 2023 11:42:28 +0300 Subject: [PATCH 34/36] 2.0 release --- CHANGELOG.md | 3 + README.md | 191 ++++++++++++++++++++++++++++++++++++------------ config/cart.php | 2 - 3 files changed, 146 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c9f194..e9b4be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `aurora` will be documented in this file. +## 2.0.0 - 2022-05-22 +- 2.0 release + ## 1.0.0 - 2022-05-22 - initial release diff --git a/README.md b/README.md index 876087a..a4beaab 100644 --- a/README.md +++ b/README.md @@ -4,85 +4,180 @@ [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/n1crack/aurora/run-tests.yml)](https://github.com/n1crack/aurora/actions) [![GitHub](https://img.shields.io/github/license/n1crack/aurora)](https://github.com/n1crack/aurora/blob/main/LICENSE.md) -The Aurora Shopping Cart for Laravel package provides a convenient way to manage a shopping cart in your Laravel applications. It allows you to add, update, remove items, apply conditions, and calculate the total and subtotal amounts. -## Support us +Aurora Cart is a flexible and feature-rich shopping cart library for Laravel. -.. +## Features + +- **Cart Management**: Easily manage the shopping cart for both guest and authenticated users. +- **Item Addition**: Add items to the cart with quantity and options support. +- **Item Modification**: Adjust item quantity, remove items, and update options. +- **Calculators**: Implement custom calculators for shipping, tax, or any other additional costs. +- **MetaData**: Attach meta information to the entire cart or individual items. +- **Snapshot and Rollback**: Save and restore the cart state for scenarios like order creation. +- **Validation**: Validate the cart integrity using checksums. +- **Unit Testing Support**: Convenient assertions for unit testing cart functionality. ## Installation -You can install the package via composer: +Install the package using Composer: ```bash composer require ozdemir/aurora ``` -You can publish the config file with: +## Configuration + +To configure Aurora Cart, publish the configuration file: ```bash php artisan vendor:publish --tag="aurora-config" ``` +This will create a cart.php file in your config directory. Here's an example configuration file with explanations: -This is the contents of the published config file: ```php - 'cart', - 'storage' => \Ozdemir\Aurora\Storages\SessionStorage::class, + 'cart_class' => Cart::class, + + 'storage' => SessionStorage::class, + + 'cache_store' => env('CART_STORE', config('cache.default')), + + 'monetary' => [ + 'precision' => env('CART_CURRENCY_PRECISION', 2), + ], + + 'session_key_generator' => GenerateSessionKey::class, + + 'checksum_generator' => GenerateChecksum::class, + 'calculate_using' => [ + // Custom calculators go here + ], ]; ``` - -## Usage +## Basic Usage ```php -Cart::add([ - 'id' => 'some-product', - 'name' => 'Some Product', - 'quantity' => 1, - 'price' => 15, -]); -$items = Cart::items(); // list of the items in the Cart +use Ozdemir\Aurora\CartItem; +use Ozdemir\Aurora\Facades\Cart; + +// Adding an item to the cart +$product = new YourProductModel(); // Replace with your actual product model +$cartItem = new CartItem($product, quantity: 2); +Cart::add($cartItem); + +// Retrieving cart information +$total = Cart::total(); +$itemCount = Cart::count(); +$items = Cart::items(); + +// Updating item quantity +Cart::update($cartItem, quantity: 3); -echo Cart::total(); +// Removing an item from the cart +Cart::remove($cartItem); ``` -### Methods -- add($data): Adds an item or items to the cart. -- update($key, $data): Updates an item in the cart. -- items(): Returns the collection of items in the cart. -- exists($key): Checks if an item exists in the cart. -- item($key): Retrieves an item from the cart by its key. -- remove($key): Removes an item from the cart. -- isEmpty(): Checks if the cart is empty. -- total(): Calculates the total amount of the cart, including conditions. -- subtotal(): Calculates the subtotal amount of the cart without conditions. -- quantity(): Calculates the total quantity of items in the cart. -- weight(): Calculates the total weight of the items in the cart. -- clear(): Clears the cart and removes all items. -- sync($data): Clears the cart and adds new items. -- condition($condition): Adds a condition to the cart. -- hasCondition($name): Checks if a condition exists in the cart. -- removeCondition($name): Removes a condition from the cart. -- itemConditions($type = null): Retrieves the item-level conditions in the cart. -- conditions($type = null): Retrieves the cart-level conditions. -- getConditionsOrder(): Gets the order of cart-level conditions. -- getItemConditionsOrder(): Gets the order of item-level conditions. -- setConditionsOrder(array $conditionsOrder): Sets the order of cart-level conditions. -- setItemConditionsOrder(array $itemConditionsOrder, $updateExisting = true): Sets the order of item-level conditions. -- serialize(): Serializes the cart instance. -- unserialize($string): Unserializes a serialized cart instance. - -## Testing +Aurora Cart supports custom calculators for calculating totals. You can add your custom calculators to the config/cart.php file under the calculate_using key. -```bash -composer test +### Example +```php +return [ + // ... + 'calculate_using' => [ + // discounts etc.. + ShippingCalculator::class + TaxCalculator::class + ], +]; +``` +```php +class ShippingCalculator +{ + public function handle($payload, Closure $next) + { + [$price, $breakdowns] = $payload; + + $shippingPrice = Shipping::first()->amount; + + $price = $price + $shippingPrice; + + $breakdowns[] = [ + 'type' => 'shipping', + 'amount' => $shippingPrice, + // any additional values.. + ]; + + return $next([$price, $breakdowns]); + } +} +``` + +```php +class TaxCalculator +{ + public function handle($payload, Closure $next) + { + [$price, $breakdowns] = $payload; + + $taxPrice = Tax::first()->amount; + + $price = $price + $taxPrice; + + $breakdowns[] = [ + 'type' => 'tax', + 'amount' => $taxPrice, + // any additional values.. + ]; + + return $next([$price, $breakdowns]); + } +} +``` +Now, your cart will use these custom calculators to calculate totals, including shipping and tax. Adjust the logic in the calculators based on your specific business requirements. + +## Breakdowns +You can retrieve the breakdowns of your cart using the Cart::breakdowns() method. Breakdowns provide a detailed summary of how the total amount is calculated, including contributions from various components such as shipping, tax, and any custom calculators you've added. + +Example + +```php +$breakdowns = Cart::breakdowns(); + +// Output the breakdowns +print_r($breakdowns); +``` +The breakdowns() method returns an array where each element represents a breakdown item. Each breakdown item typically includes a label and value, providing transparency into how different factors contribute to the overall total. + +Here's a hypothetical example output: + +```php +Array ( + [0] => Array ( + [label] => Shipping + [value] => 10 + ) + [1] => Array ( + [label] => Tax + [value] => 15 + ) + // Additional breakdown items based on your custom calculators + // ... +) ``` +Adjust the output format and contents as needed for your specific use case. ## Changelog diff --git a/config/cart.php b/config/cart.php index 5fadc70..7ccd02b 100644 --- a/config/cart.php +++ b/config/cart.php @@ -15,8 +15,6 @@ 'cache_store' => env('CART_STORE', config('cache.default')), 'monetary' => [ - 'class' => Money::class, - 'precision' => env('CART_CURRENCY_PRECISION', 2), ], From 48c40bff53f2d04c7cf3a75b69ee48c824231d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 28 Nov 2023 11:43:15 +0300 Subject: [PATCH 35/36] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index a4beaab..14b5cd6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ Aurora Cart is a flexible and feature-rich shopping cart library for Laravel. - **MetaData**: Attach meta information to the entire cart or individual items. - **Snapshot and Rollback**: Save and restore the cart state for scenarios like order creation. - **Validation**: Validate the cart integrity using checksums. -- **Unit Testing Support**: Convenient assertions for unit testing cart functionality. ## Installation From cc01ff7e118bb11c355ebd3769ba9f4d1a451f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C3=96zdemir?= Date: Tue, 28 Nov 2023 11:48:38 +0300 Subject: [PATCH 36/36] Update ItemTest.php --- tests/ItemTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ItemTest.php b/tests/ItemTest.php index 2a765d5..96e06a9 100644 --- a/tests/ItemTest.php +++ b/tests/ItemTest.php @@ -16,7 +16,7 @@ $item = new CartItem($product, quantity: 2), ); - expect(get_class($item))->toImplement(\Ozdemir\Aurora\Contracts\CartItemInterface::class) + expect($item)->toBeInstanceOf(\Ozdemir\Aurora\Contracts\CartItemInterface::class) ->and(Cart::quantity())->toBe(2) ->and(Cart::isEmpty())->toBeFalse();