I cart facade

This commit is contained in:
Fabian @ Blax Software 2025-12-17 09:24:42 +01:00
parent 0e776b6d84
commit 7478f69bcf
5 changed files with 102 additions and 33 deletions

View File

@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Facade;
* @method static \Blax\Shop\Models\CartItem|true remove(\Illuminate\Database\Eloquent\Model $product, int $quantity = 1, array $parameters = []) * @method static \Blax\Shop\Models\CartItem|true remove(\Illuminate\Database\Eloquent\Model $product, int $quantity = 1, array $parameters = [])
* @method static \Blax\Shop\Models\CartItem update(\Blax\Shop\Models\CartItem $cartItem, int $quantity) * @method static \Blax\Shop\Models\CartItem update(\Blax\Shop\Models\CartItem $cartItem, int $quantity)
* @method static int clear(\Blax\Shop\Models\Cart|null $cart = null) * @method static int clear(\Blax\Shop\Models\Cart|null $cart = null)
* @method static void clearSession()
* @method static \Illuminate\Support\Collection|mixed checkout(\Blax\Shop\Models\Cart|null $cart = null) * @method static \Illuminate\Support\Collection|mixed checkout(\Blax\Shop\Models\Cart|null $cart = null)
* @method static float total(\Blax\Shop\Models\Cart|null $cart = null) * @method static float total(\Blax\Shop\Models\Cart|null $cart = null)
* @method static int itemCount(\Blax\Shop\Models\Cart|null $cart = null) * @method static int itemCount(\Blax\Shop\Models\Cart|null $cart = null)

View File

@ -5,6 +5,7 @@ namespace Blax\Shop\Models;
use Blax\Shop\Contracts\Cartable; use Blax\Shop\Contracts\Cartable;
use Blax\Shop\Enums\CartStatus; use Blax\Shop\Enums\CartStatus;
use Blax\Shop\Enums\ProductType; use Blax\Shop\Enums\ProductType;
use Blax\Shop\Services\CartService;
use Blax\Workkit\Traits\HasExpiration; use Blax\Workkit\Traits\HasExpiration;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
@ -176,6 +177,17 @@ class Cart extends Model
}); });
} }
/**
* Store the cart ID in the session for retrieval across requests
*
* @param Cart $cart
* @return void
*/
public static function setSession(Cart $cart): void
{
session([CartService::CART_SESSION_KEY => $cart->id]);
}
/** /**
* Add an item to the cart or increase quantity if it already exists. * Add an item to the cart or increase quantity if it already exists.
* *
@ -226,7 +238,7 @@ class Cart extends Model
); );
} }
} }
// Add items one at a time for progressive pricing // Add items one at a time for progressive pricing
$lastCartItem = null; $lastCartItem = null;
for ($i = 0; $i < $quantity; $i++) { for ($i = 0; $i < $quantity; $i++) {
@ -357,7 +369,7 @@ class Cart extends Model
// Get price for the next available item // Get price for the next available item
$pricePerDay = $cartable->getNextAvailablePoolPrice($currentQuantityInCart, null, $from, $until); $pricePerDay = $cartable->getNextAvailablePoolPrice($currentQuantityInCart, null, $from, $until);
$regularPricePerDay = $cartable->getNextAvailablePoolPrice($currentQuantityInCart, false, $from, $until) ?? $pricePerDay; $regularPricePerDay = $cartable->getNextAvailablePoolPrice($currentQuantityInCart, false, $from, $until) ?? $pricePerDay;
// If no price found from pool items, try the pool's direct price as fallback // If no price found from pool items, try the pool's direct price as fallback
if ($pricePerDay === null && $cartable->hasPrice()) { if ($pricePerDay === null && $cartable->hasPrice()) {
$pricePerDay = $cartable->defaultPrice()->first()?->getCurrentPrice($cartable->isOnSale()); $pricePerDay = $cartable->defaultPrice()->first()?->getCurrentPrice($cartable->isOnSale());

View File

@ -16,6 +16,7 @@ use Blax\Shop\Exceptions\HasNoDefaultPriceException;
use Blax\Shop\Exceptions\HasNoPriceException; use Blax\Shop\Exceptions\HasNoPriceException;
use Blax\Shop\Exceptions\InvalidBookingConfigurationException; use Blax\Shop\Exceptions\InvalidBookingConfigurationException;
use Blax\Shop\Exceptions\InvalidPoolConfigurationException; use Blax\Shop\Exceptions\InvalidPoolConfigurationException;
use Blax\Shop\Services\CartService;
use Blax\Shop\Traits\HasCategories; use Blax\Shop\Traits\HasCategories;
use Blax\Shop\Traits\HasPrices; use Blax\Shop\Traits\HasPrices;
use Blax\Shop\Traits\HasPricingStrategy; use Blax\Shop\Traits\HasPricingStrategy;
@ -374,9 +375,22 @@ class Product extends Model implements Purchasable, Cartable
{ {
// If this is a pool product, use cart-aware pricing if cart is provided // If this is a pool product, use cart-aware pricing if cart is provided
if ($this->isPool()) { if ($this->isPool()) {
// If no cart provided, try to get the current user's cart // If no cart provided, try to get the cart from session first, then user's cart
if (!$cart && auth()->check()) { if (!$cart) {
$cart = auth()->user()->currentCart(); // Try session first
$cartId = session(CartService::CART_SESSION_KEY);
if ($cartId) {
$cart = \Blax\Shop\Models\Cart::find($cartId);
// Make sure the cart is valid (not expired/converted)
if ($cart && ($cart->isExpired() || $cart->isConverted())) {
$cart = null;
}
}
// Fall back to authenticated user's cart if no valid session cart
if (!$cart && auth()->check()) {
$cart = auth()->user()->currentCart();
}
} }
if ($cart) { if ($cart) {

View File

@ -14,47 +14,83 @@ use Blax\Shop\Exceptions\NotPurchasable;
use Blax\Shop\Models\Product; use Blax\Shop\Models\Product;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
class CartService class CartService
{ {
/** /**
* Get current authenticated user's cart * Session key for storing the current cart ID
* Throws exception if no user is authenticated */
public const CART_SESSION_KEY = 'blax_shop_cart_id';
/**
* Get current cart from session, or fall back to authenticated user's cart
* Throws exception if no cart found in session and no user is authenticated
* *
* @return Cart * @return Cart
* @throws \Exception * @throws \Exception
*/ */
public function current(): Cart public function current(): Cart
{ {
$user = auth()->user(); // Try to get cart from session first
$cartId = session(self::CART_SESSION_KEY);
if (!$user) { if ($cartId) {
throw new \Exception('No authenticated user found. Use guest() for guest carts or provide a cart ID.'); $cart = Cart::find($cartId);
if ($cart && !$cart->isExpired() && !$cart->isConverted()) {
// If user is authenticated and session cart is a guest cart, clear it
if (Auth::check() && !$cart->customer_id) {
session()->forget(self::CART_SESSION_KEY);
} else {
return $cart;
}
} else {
// Clear expired or converted cart from session
session()->forget(self::CART_SESSION_KEY);
}
} }
return $user->currentCart(); // Fall back to authenticated user's cart
$user = Auth::user();
if (!$user) {
return self::guest();
}
$cart = $user->currentCart();
// Store the cart ID in session for future requests
session([self::CART_SESSION_KEY => $cart->id]);
return $cart;
} }
/** /**
* Get or create a guest cart by session ID * Get or create a guest cart by session ID
* If no session ID provided, uses session()->getId() * If no session ID provided, uses session()->getId()
* Stores the cart ID in the session for future requests
* *
* @param string|null $sessionId * @param string|null $sessionId
* @return Cart * @return Cart
*/ */
public function guest(?string $sessionId = null): Cart public function guest(?string $sessionId = null): Cart
{ {
$sessionId = $sessionId ?? session()->getId(); $sessionId = $sessionId ?? session(CartService::CART_SESSION_KEY) ?? session()->getId();
return Cart::firstOrCreate([ $cart = Cart::firstOrCreate([
'session_id' => $sessionId, 'session_id' => $sessionId,
'customer_id' => null, 'customer_id' => null,
'customer_type' => null, 'customer_type' => null,
]); ]);
// Store cart ID in session
Cart::setSession($cart);
return $cart;
} }
/** /**
* Get cart for specific user * Get cart for specific user
* Stores the cart ID in the session for future requests
* *
* @param Authenticatable $user * @param Authenticatable $user
* @return Cart * @return Cart
@ -65,7 +101,11 @@ class CartService
throw new \Exception('User model must have shopping capabilities'); throw new \Exception('User model must have shopping capabilities');
} }
return $user->currentCart(); $cart = $user->currentCart();
// Store cart ID in session
Cart::setSession($cart);
return $cart;
} }
/** /**
@ -80,8 +120,8 @@ class CartService
} }
/** /**
* Add item to current user's cart (throws exception if no user) * Add item to current cart (from session or authenticated user)
* For guests, use guest() first: Cart::guest()->add($product) * For guests without session, use guest() first: Cart::guest()->add($product)
* *
* @param Model&Cartable $product * @param Model&Cartable $product
* @param int $quantity * @param int $quantity
@ -92,23 +132,19 @@ class CartService
*/ */
public function add(Model $product, int $quantity = 1, array $parameters = []): CartItem public function add(Model $product, int $quantity = 1, array $parameters = []): CartItem
{ {
$user = auth()->user(); $cart = $this->current();
if (!$user) {
throw new \Exception('No authenticated user found. Use guest() for guest carts.');
}
// Validate pricing before adding to cart // Validate pricing before adding to cart
if ($product instanceof Product) { if ($product instanceof Product) {
$product->validatePricing(throwExceptions: true); $product->validatePricing(throwExceptions: true);
} }
return $user->addToCart($product, $quantity, $parameters); return $cart->addToCart($product, $quantity, $parameters);
} }
/** /**
* Remove item from current user's cart (throws exception if no user) * Remove item from current cart (from session or authenticated user)
* For guests, use guest() first: Cart::guest()->remove($product) * For guests without session, use guest() first: Cart::guest()->remove($product)
* *
* @param Model&Cartable $product * @param Model&Cartable $product
* @param int $quantity * @param int $quantity
@ -117,13 +153,8 @@ class CartService
*/ */
public function remove(Model $product, int $quantity = 1, array $parameters = []) public function remove(Model $product, int $quantity = 1, array $parameters = [])
{ {
$user = auth()->user(); $cart = $this->current();
return $cart->removeFromCart($product, $quantity, $parameters);
if (!$user) {
throw new \Exception('No authenticated user found. Use guest() for guest carts.');
}
return $user->currentCart()->removeFromCart($product, $quantity, $parameters);
} }
/** /**
@ -397,6 +428,17 @@ class CartService
return empty($this->validateBookings($cart)); return empty($this->validateBookings($cart));
} }
/**
* Clear the cart from session (unlink cart from session)
* This does not delete the cart, just removes it from the session
*
* @return void
*/
public function clearSession(): void
{
session()->forget(self::CART_SESSION_KEY);
}
/** /**
* Add a booking product to cart with timespan * Add a booking product to cart with timespan
* *

View File

@ -34,7 +34,7 @@ class CartFacadeTest extends TestCase
auth()->logout(); auth()->logout();
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
$this->expectExceptionMessage('No authenticated user found'); $this->expectExceptionMessage('No cart in session and no authenticated user found');
Cart::current(); Cart::current();
} }