Skip to content

Commit

Permalink
Added configuration support to the Cart::addItem() method
Browse files Browse the repository at this point in the history
  • Loading branch information
fulopattila122 committed Mar 19, 2024
1 parent efd4880 commit 1017416
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 3 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Dropped Enum v3 Support
- Added PHP 8.3 Support
- Added Laravel 11 Support
- Added Cart item configuration support (different configurations constitute separate cart items) to the `Cart::addItem()` method
- Changed minimum Laravel version to v10.38.2
- Changed minimal Enum requirement to v4.2
- Removed the throwing of `CartUpdated` event when destroying a cart (`CartDeleting` and `CartDeleted` remains)
Expand Down
59 changes: 56 additions & 3 deletions Models/Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Konekt\Enum\Eloquent\CastsEnums;
use Vanilo\Cart\Contracts\Cart as CartContract;
Expand All @@ -33,7 +34,7 @@ class Cart extends Model implements CartContract
protected $guarded = ['id'];

protected $enums = [
'state' => 'CartStateProxy@enumClass'
'state' => 'CartStateProxy@enumClass',
];

public function items(): HasMany
Expand All @@ -53,7 +54,7 @@ public function itemCount(): int

public function addItem(Buyable $product, int|float $qty = 1, array $params = []): CartItemContract
{
$item = $this->items()->ofCart($this)->byProduct($product)->first();
$item = $this->resolveCartItem($product, $params);

if ($item) {
$item->quantity += $qty;
Expand Down Expand Up @@ -148,6 +149,58 @@ public function scopeOfUser($query, $user)
return $query->where('user_id', is_object($user) ? $user->id : $user);
}

protected function resolveCartItem(Buyable $buyable, array $parameters): ?CartItemContract
{
/** @var Collection $existingCartItems */
$existingCartItems = $this->items()->ofCart($this)->byProduct($buyable)->get();
if ($existingCartItems->isEmpty()) {
return null;
}

$itemConfig = Arr::get($parameters, 'attributes.configuration');

if (1 === $existingCartItems->count()) {
$item = $this->items()->ofCart($this)->byProduct($buyable)->first();

return $this->configurationsMatch($item->configuration(), $itemConfig) ? $item : null;
}

foreach ($existingCartItems as $item) {
if ($this->configurationsMatch($item->configuration(), $itemConfig)) {
return $item;
}
}

return null;
}

protected function configurationsMatch(?array $config1, ?array $config2): bool
{
if (empty($config1) && empty($config2)) {
return true;
} elseif (empty($config1) && !empty($config2)) {
return false;
} elseif (empty($config2) && !empty($config1)) {
return false;
}

if (array_is_list($config1)) {
if (!array_is_list($config2)) {
return false;
}

return empty(array_diff($config1, $config2)) && empty(array_diff($config2, $config1));
} else { //Config 1 is associative
if (array_is_list($config2)) {
return false;
}

return empty(array_diff_assoc($config1, $config2)) && empty(array_diff_assoc($config2, $config1));
}

return false;
}

/**
* Returns the default attributes of a Buyable for a cart item
*
Expand All @@ -162,7 +215,7 @@ protected function getDefaultCartItemAttributes(Buyable $product, $qty)
'product_type' => $product->morphTypeName(),
'product_id' => $product->getId(),
'quantity' => $qty,
'price' => $product->getPrice()
'price' => $product->getPrice(),
];
}

Expand Down
146 changes: 146 additions & 0 deletions Tests/CartItemConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/**
* Contains the CartItemConfigurationTest class.
*
* @copyright Copyright (c) 2024 Vanilo UG
* @author Attila Fulop
* @license MIT
* @since 2024-03-19
*
*/

namespace Vanilo\Cart\Tests;

use Vanilo\Cart\Facades\Cart;
use Vanilo\Cart\Tests\Dummies\Product;

class CartItemConfigurationTest extends TestCase
{
/** @test */
public function a_cart_items_configuration_can_be_saved()
{
$product = Product::create([
'name' => 'Tasty Burger',
'price' => 12.99
]);

$item = Cart::addItem($product, 1, ['attributes' => [
'configuration' => ['extra_cheese', 'bacon'],
]]);

$this->assertEquals(['extra_cheese', 'bacon'], $item->configuration());
}

/** @test */
public function when_adding_a_product_that_has_a_different_configuration_then_two_separate_cart_items_will_be_added()
{
$product = Product::create([
'name' => 'Eleven Burger',
'price' => 17.99
]);

Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['no_pommes', 'extra_coleslaw']]]);

$this->assertCount(2, Cart::getItems());
$this->assertEquals(['extra_coleslaw'], Cart::getItems()->first()->configuration());
$this->assertEquals(['no_pommes', 'extra_coleslaw'], Cart::getItems()->last()->configuration());
}

/** @test */
public function when_adding_a_product_where_one_has_an_associative_array_config_and_the_other_one_a_list_then_two_separate_items_will_be_created()
{
$product = Product::create([
'name' => 'Juicy Lucy',
'price' => 15.99
]);

Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['pommes' => false, 'bacon' => 2]]]);

$this->assertCount(2, Cart::getItems());
$this->assertEquals(['extra_coleslaw'], Cart::getItems()->first()->configuration());
$this->assertEquals(['pommes' => false, 'bacon' => 2], Cart::getItems()->last()->configuration());

Cart::clear();

// Now doing it in the opposite order

Cart::addItem($product, 1, ['attributes' => ['configuration' => ['pommes' => false, 'bacon' => 2]]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);

$this->assertCount(2, Cart::getItems());
$this->assertEquals(['pommes' => false, 'bacon' => 2], Cart::getItems()->first()->configuration());
$this->assertEquals(['extra_coleslaw'], Cart::getItems()->last()->configuration());
}

/** @test */
public function it_creates_one_item_if_the_configurations_match()
{
$product = Product::create([
'name' => 'Italian Burger',
'price' => 16.99
]);

Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);

$this->assertCount(1, Cart::getItems());
$this->assertEquals(['extra_coleslaw'], Cart::getItems()->first()->configuration());
}

/** @test */
public function it_groups_configurationless_entries_and_keeps_configured_ones_separated()
{
$product = Product::create([
'name' => 'Wendy\'s Burger',
'price' => 16.99
]);

Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product);
Cart::addItem($product);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['extra_coleslaw']]]);
Cart::addItem($product);

$this->assertCount(2, Cart::getItems());
$this->assertEquals(['extra_coleslaw'], Cart::getItems()->first()->configuration());
$this->assertEquals(3, Cart::getItems()->first()->getQuantity());
$this->assertEquals([], Cart::getItems()->last()->configuration());
$this->assertEquals(4, Cart::getItems()->last()->getQuantity());
}

/** @test */
public function it_distinguishes_configurations_across_multiple_add_to_cart_calls()
{
$product = Product::create([
'name' => 'McFarm',
'price' => 12.99
]);

Cart::addItem($product);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['double_meat']]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['mustard_sauce' => false]]]);
Cart::addItem($product);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['mustard_sauce' => false]]]);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['mustard_sauce' => false]]]);
Cart::addItem($product);
Cart::addItem($product, 1, ['attributes' => ['configuration' => ['double_meat']]]);

$this->assertCount(3, Cart::getItems());

$this->assertEquals([], Cart::getItems()->get(0)->configuration());
$this->assertEquals(3, Cart::getItems()->get(0)->getQuantity());

$this->assertEquals(['double_meat'], Cart::getItems()->get(1)->configuration());
$this->assertEquals(2, Cart::getItems()->get(1)->getQuantity());

$this->assertEquals(['mustard_sauce' => false], Cart::getItems()->get(2)->configuration());
$this->assertEquals(3, Cart::getItems()->get(2)->getQuantity());
}
}

0 comments on commit 1017416

Please sign in to comment.