Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions packages/core/src/Models/Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,23 @@ public function recalculate(): Cart
return $this->calculate(force: true);
}

/**
* Refresh the cart and reload the relations used during calculation.
*
* Eloquent's refresh() only reloads relations that are already loaded,
* and only one level deep, so nested relations configured in
* `lunar.cart.eager_load` (e.g. `lines.purchasable.prices.currency`) can
* end up only partially loaded after a refresh. Reloading them
* explicitly avoids a lazy load being triggered the next time the cart
* is calculated.
*/
public function refreshForCalculation(): Cart
{
return $this->refresh()->load(
config('lunar.cart.eager_load', [])
);
}

public function isCalculated(): bool
{
return ! blank($this->total) && $this->lines->every(
Expand All @@ -423,7 +440,7 @@ public function add(Purchasable $purchasable, int $quantity = 1, array $meta = [
return app(
config('lunar.cart.actions.add_to_cart', AddOrUpdatePurchasable::class)
)->execute($this, $purchasable, $quantity, $meta)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

public function addLines(iterable $lines): Cart
Expand All @@ -439,7 +456,7 @@ public function addLines(iterable $lines): Cart
});
});

return $this->refresh()->recalculate();
return $this->refreshForCalculation()->recalculate();
}

public function remove(int $cartLineId, bool $refresh = true): Cart
Expand All @@ -454,7 +471,7 @@ public function remove(int $cartLineId, bool $refresh = true): Cart
return app(
config('lunar.cart.actions.remove_from_cart', RemovePurchasable::class)
)->execute($this, $cartLineId)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

/**
Expand All @@ -474,7 +491,7 @@ public function updateLine(int $cartLineId, int $quantity, ?array $meta = null,
return app(
config('lunar.cart.actions.update_cart_line', UpdateCartLine::class)
)->execute($cartLineId, $quantity, $meta)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

public function updateLines(Collection $lines): Cart
Expand All @@ -490,14 +507,14 @@ public function updateLines(Collection $lines): Cart
});
});

return $this->refresh()->recalculate();
return $this->refreshForCalculation()->recalculate();
}

public function clear(): Cart
{
$this->lines()->delete();

return $this->refresh()->recalculate();
return $this->refreshForCalculation()->recalculate();
}

/**
Expand All @@ -518,7 +535,7 @@ public function associate(LunarUser $user, string $policy = 'merge', bool $refre
return app(
config('lunar.cart.actions.associate_user', AssociateUser::class)
)->execute($this, $user, $policy)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

public function setCustomer(Customer $customer): Cart
Expand All @@ -533,7 +550,7 @@ public function setCustomer(Customer $customer): Cart

$this->customer()->associate($customer)->save();

return $this->refresh()->recalculate();
return $this->refreshForCalculation()->recalculate();
}

public function addAddress(array|Addressable $address, string $type, bool $refresh = true): Cart
Expand All @@ -549,7 +566,7 @@ public function addAddress(array|Addressable $address, string $type, bool $refre
return app(
config('lunar.cart.actions.add_address', AddAddress::class)
)->execute($this, $address, $type)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

public function setShippingAddress(array|Addressable $address, bool $clearTaxZone = true): Cart
Expand Down Expand Up @@ -578,7 +595,7 @@ public function setShippingOption(ShippingOption $option, bool $refresh = true):
return app(
config('lunar.cart.actions.set_shipping_option', SetShippingOption::class)
)->execute($this, $option)
->then(fn () => $refresh ? $this->refresh()->recalculate() : $this);
->then(fn () => $refresh ? $this->refreshForCalculation()->recalculate() : $this);
}

public function getShippingOption(): ?ShippingOption
Expand All @@ -597,7 +614,7 @@ public function createOrder(
bool $allowMultipleOrders = false,
?int $orderIdToUpdate = null
): Order {
$cart = $this->refresh()->recalculate();
$cart = $this->refreshForCalculation()->recalculate();

foreach (config('lunar.cart.validators.order_create', [
ValidateCartForOrderCreation::class,
Expand Down Expand Up @@ -707,6 +724,6 @@ public function setTaxZone(?TaxZoneContract $taxZone, bool $refresh = true): Car

$this->save();

return $this->refresh()->recalculate();
return $this->refreshForCalculation()->recalculate();
}
}
73 changes: 73 additions & 0 deletions tests/core/Unit/Models/CartTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1360,3 +1360,76 @@
expect($cart->load('lines')->lines->pluck('id')->all())
->toBe($expectedOrder);
});

test('refreshing the cart for calculation reloads nested relations dropped by a plain refresh', function () {
// Regression test for #2222: adding a second product to a cart in the
// same request used to throw a LazyLoadingException. Cart::add(),
// Cart::remove() etc. all called $this->refresh() before recalculating,
// but Eloquent's refresh() only reloads relations that are already
// loaded, and only one level deep. So a nested relation configured in
// lunar.cart.eager_load, such as lines.purchasable.prices, ended up
// dropped after a refresh and got lazy-loaded the next time the cart
// was calculated.
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasable = ProductVariant::factory()->create(['stock' => 1]);

Price::factory()->create([
'price' => 100,
'min_quantity' => 1,
'currency_id' => $currency->id,
'priceable_type' => $purchasable->getMorphClass(),
'priceable_id' => $purchasable->id,
]);

$cart->lines()->create([
'purchasable_type' => $purchasable->getMorphClass(),
'purchasable_id' => $purchasable->id,
'quantity' => 1,
]);

$cart->load(config('lunar.cart.eager_load'));

// A plain Eloquent refresh() drops the nested relations.
$cart->refresh();
expect($cart->lines->first()->relationLoaded('purchasable'))->toBeFalse();

// refreshForCalculation() reapplies the full eager_load config, so the
// nested relations survive the refresh.
$refreshed = $cart->refreshForCalculation();
$line = $refreshed->lines->first();

expect($line->relationLoaded('purchasable'))->toBeTrue()
->and($line->purchasable->relationLoaded('prices'))->toBeTrue()
->and($line->purchasable->prices->first()->relationLoaded('currency'))->toBeTrue();
});

test('can add multiple purchasables to a cart without triggering lazy loading', function () {
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasableA = ProductVariant::factory()->create(['stock' => 1]);
$purchasableB = ProductVariant::factory()->create(['stock' => 1]);

foreach ([$purchasableA, $purchasableB] as $purchasable) {
Price::factory()->create([
'price' => 100,
'min_quantity' => 1,
'currency_id' => $currency->id,
'priceable_type' => $purchasable->getMorphClass(),
'priceable_id' => $purchasable->id,
]);
}

$cart->add($purchasableA, 1);
$cart->add($purchasableB, 1);

expect($cart->lines)->toHaveCount(2);
})->throwsNoExceptions();