From 65bc8c63dec7df2ce22c59a6e7b5873ecffc644d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=22DualFroz=22=20Fox?= Date: Thu, 17 Sep 2026 19:42:53 +0200 Subject: [PATCH] fix(cart): refresh config options of a cart item that changed (closes #1207) (#1326) --- app/Livewire/Products/Checkout.php | 27 +++-- tests/Feature/CartConfigOptionChangeTest.php | 119 +++++++++++++++++++ 2 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/CartConfigOptionChangeTest.php diff --git a/app/Livewire/Products/Checkout.php b/app/Livewire/Products/Checkout.php index c34cf89..bac7185 100644 --- a/app/Livewire/Products/Checkout.php +++ b/app/Livewire/Products/Checkout.php @@ -63,17 +63,6 @@ class Checkout extends Component $this->plan = $this->plan_id ? $this->product->plans->findOrFail($this->plan_id) : $this->product->plans->first(); $this->plan_id = $this->plan->id; - // Prepare the config options - $this->configOptions = $this->product->configOptions->mapWithKeys(function ($option) { - if (in_array($option->type, ['text', 'number'])) { - return [$option->id => $this->configOptions[$option->id] ?? null]; - } - if ($option->type === 'checkbox') { - return [$option->id => isset($this->configOptions[$option->id]) && in_array($this->configOptions[$option->id], [true, 'true'], true) ? true : false]; - } - - return [$option->id => $this->configOptions[$option->id] ?? $option->children->first()->id]; - })->toArray(); foreach ($this->getCheckoutConfig() as $config) { if (in_array($config['type'], ['select', 'radio'])) { $this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? array_key_first($config['options']); @@ -82,6 +71,22 @@ class Checkout extends Component } } } + + // Prepare the config options. Also done for an item that is already in the cart, because the + // product may have gained, lost or changed options since it was added. + $this->configOptions = $this->product->configOptions->mapWithKeys(function ($option) { + if (in_array($option->type, ['text', 'number'])) { + return [$option->id => $this->configOptions[$option->id] ?? null]; + } + if ($option->type === 'checkbox') { + return [$option->id => isset($this->configOptions[$option->id]) && in_array($this->configOptions[$option->id], [true, 'true'], true) ? true : false]; + } + + $value = $this->configOptions[$option->id] ?? null; + + return [$option->id => $option->children->contains('id', $value) ? $value : $option->children->first()->id]; + })->toArray(); + // Update the pricing $this->updatePricing(); diff --git a/tests/Feature/CartConfigOptionChangeTest.php b/tests/Feature/CartConfigOptionChangeTest.php new file mode 100644 index 0000000..f6b84c6 --- /dev/null +++ b/tests/Feature/CartConfigOptionChangeTest.php @@ -0,0 +1,119 @@ +product = $this->createProduct(); + + $this->option = ConfigOption::create([ + 'name' => 'Location', + 'type' => 'radio', + 'sort' => 0, + ]); + $this->option->products()->attach($this->product->product->id); + + foreach (['Amsterdam', 'Frankfurt'] as $sort => $name) { + $this->createOptionValue($name, $sort); + } + } + + private function createOptionValue(string $name, int $sort): ConfigOption + { + $value = ConfigOption::create([ + 'name' => $name, + 'sort' => $sort, + 'parent_id' => $this->option->id, + ]); + + $plan = Plan::factory()->create([ + 'priceable_id' => $value->id, + 'priceable_type' => ConfigOption::class, + 'name' => $name, + 'billing_unit' => 'month', + 'billing_period' => 1, + 'type' => 'recurring', + ]); + + Price::factory()->create([ + 'plan_id' => $plan->id, + 'price' => 1.00, + 'currency_code' => 'USD', + ]); + + return $value; + } + + private function addToCart(ConfigOption $value): array + { + Livewire::test('products.checkout', [ + 'category' => $this->product->product->category, + 'product' => $this->product->product->slug, + ]) + ->set('configOptions.' . $this->option->id, $value->id) + ->call('checkout'); + + $cart = Cart::firstOrFail(); + Once::flush(); + + return [$cart, $cart->items()->firstOrFail()]; + } + + private function editCartItem(Cart $cart, $item) + { + return Livewire::withCookie('cart', $cart->ulid)->test('products.checkout', [ + 'category' => $this->product->product->category, + 'product' => $this->product->product->slug, + 'cartProductKey' => $item->id, + ]); + } + + public function test_cart_item_can_be_edited_after_its_option_value_is_removed(): void + { + $values = $this->option->children()->get(); + [$cart, $item] = $this->addToCart($values[0]); + + // The option value the item was ordered with is removed from the product. + $values[0]->delete(); + + $this->editCartItem($cart, $item) + ->assertSet('configOptions.' . $this->option->id, $values[1]->id) + ->call('checkout') + ->assertHasNoErrors(); + + $this->assertSame(1, $cart->items()->count()); + $this->assertSame($values[1]->id, $cart->items()->first()->config_options[0]['value']); + } + + public function test_cart_item_keeps_its_option_value_when_it_is_still_available(): void + { + $values = $this->option->children()->get(); + [$cart, $item] = $this->addToCart($values[1]); + + $this->editCartItem($cart, $item) + ->assertSet('configOptions.' . $this->option->id, $values[1]->id) + ->call('checkout') + ->assertHasNoErrors(); + + $this->assertSame(1, $cart->items()->count()); + $this->assertSame($values[1]->id, $cart->items()->first()->config_options[0]['value']); + } +}