laravel-shop/src/Models/Cart.php

328 lines
10 KiB
PHP
Raw Normal View History

2025-11-21 10:49:41 +00:00
<?php
namespace Blax\Shop\Models;
use Blax\Shop\Contracts\Cartable;
use Blax\Shop\Enums\CartStatus;
use Blax\Shop\Enums\ProductType;
2025-11-21 10:49:41 +00:00
use Blax\Workkit\Traits\HasExpiration;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
2025-11-29 11:05:02 +00:00
use Illuminate\Database\Eloquent\Factories\HasFactory;
2025-11-21 10:49:41 +00:00
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Cart extends Model
{
2025-11-29 11:05:02 +00:00
use HasUuids, HasExpiration, HasFactory;
2025-11-21 10:49:41 +00:00
protected $fillable = [
'session_id',
'customer_type',
'customer_id',
'currency',
'status',
'last_activity_at',
'expires_at',
'converted_at',
'meta',
];
protected $casts = [
'status' => CartStatus::class,
2025-11-21 10:49:41 +00:00
'expires_at' => 'datetime',
'converted_at' => 'datetime',
'last_activity_at' => 'datetime',
'meta' => 'object',
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = config('shop.tables.carts', 'carts');
}
public function customer(): MorphTo
{
return $this->morphTo();
}
// Alias for backward compatibility
public function user()
{
return $this->customer();
}
public function items(): HasMany
{
return $this->hasMany(config('shop.models.cart_item'), 'cart_id');
}
public function purchases(): HasMany
{
return $this->hasMany(config('shop.models.product_purchase', \Blax\Shop\Models\ProductPurchase::class), 'cart_id');
}
public function getTotal(): float
{
return $this->items->sum(function ($item) {
2025-11-28 09:24:07 +00:00
return $item->subtotal;
2025-11-21 10:49:41 +00:00
});
}
public function getTotalItems(): int
{
return $this->items->sum('quantity');
}
2025-11-29 11:05:02 +00:00
public function getUnpaidAmount(): float
{
$paidAmount = $this->purchases()
->whereColumn('total_amount', '!=', 'amount_paid')
->sum('total_amount');
return max(0, $this->getTotal() - $paidAmount);
}
public function getPaidAmount(): float
{
return $this->purchases()
->whereColumn('total_amount', '!=', 'amount_paid')
->sum('total_amount');
}
2025-11-21 10:49:41 +00:00
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
public function isConverted(): bool
{
return !is_null($this->converted_at);
}
public function scopeActive($query)
{
return $query->whereNull('converted_at')
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
}
public function scopeForUser($query, $userOrId)
{
if (is_object($userOrId)) {
return $query->where('customer_id', $userOrId->id)
->where('customer_type', get_class($userOrId));
}
// If just an ID is passed, try to determine the user model class
$userModel = config('auth.providers.users.model', \Workbench\App\Models\User::class);
return $query->where('customer_id', $userOrId)
->where('customer_type', $userModel);
}
2025-11-22 17:09:45 +00:00
2025-11-29 11:05:02 +00:00
public static function scopeUnpaid($query)
{
return $query->whereDoesntHave('purchases', function ($q) {
$q->whereColumn('total_amount', '!=', 'amount_paid');
});
}
2025-11-22 17:09:45 +00:00
protected static function booted()
{
static::deleting(function ($cart) {
$cart->items()->delete();
});
}
2025-12-09 08:42:59 +00:00
/**
* Add an item to the cart or increase quantity if it already exists.
*
* @param Model&Cartable $cartable The item to add to cart
* @param int $quantity The quantity to add
* @param array<string, mixed> $parameters Additional parameters for the cart item
* @return CartItem
* @throws \Exception If the item doesn't implement Cartable interface
*/
public function addToCart(
2025-11-29 11:05:02 +00:00
Model $cartable,
2025-12-09 08:42:59 +00:00
int $quantity = 1,
array $parameters = []
2025-11-29 11:05:02 +00:00
): CartItem {
// $cartable must implement Cartable
if (! $cartable instanceof Cartable) {
throw new \Exception("Item must implement the Cartable interface.");
}
2025-12-09 08:42:59 +00:00
// Check if item already exists in cart with same parameters
$existingItem = $this->items()
->where('purchasable_id', $cartable->getKey())
->where('purchasable_type', get_class($cartable))
->get()
->first(function ($item) use ($parameters) {
$existingParams = is_array($item->parameters)
? $item->parameters
: (array) $item->parameters;
// Sort both arrays to ensure consistent comparison
ksort($existingParams);
ksort($parameters);
return $existingParams === $parameters;
});
if ($existingItem) {
// Update quantity and subtotal
$newQuantity = $existingItem->quantity + $quantity;
$existingItem->update([
'quantity' => $newQuantity,
'subtotal' => ($cartable->getCurrentPrice()) * $newQuantity,
]);
return $existingItem->fresh();
}
// Create new cart item
$cartItem = $this->items()->create([
'purchasable_id' => $cartable->getKey(),
'purchasable_type' => get_class($cartable),
'quantity' => $quantity,
2025-12-09 08:42:59 +00:00
'price' => $cartable->getCurrentPrice(),
'regular_price' => $cartable->getCurrentPrice(false) ?? $cartable->unit_amount,
'subtotal' => ($cartable->getCurrentPrice()) * $quantity,
'parameters' => $parameters,
]);
2025-12-09 08:42:59 +00:00
return $cartItem->fresh();
}
2025-11-29 11:05:02 +00:00
2025-12-09 09:30:53 +00:00
public function removeFromCart(
Model $cartable,
int $quantity = 1,
array $parameters = []
): CartItem|true {
$item = $this->items()
->where('purchasable_id', $cartable->getKey())
->where('purchasable_type', get_class($cartable))
->get()
->first(function ($item) use ($parameters) {
$existingParams = is_array($item->parameters)
? $item->parameters
: (array) $item->parameters;
ksort($existingParams);
ksort($parameters);
return $existingParams === $parameters;
});
if ($item) {
if ($item->quantity > $quantity) {
// Decrease quantity
$newQuantity = $item->quantity - $quantity;
$item->update([
'quantity' => $newQuantity,
'subtotal' => ($cartable->getCurrentPrice()) * $newQuantity,
]);
} else {
// Remove item from cart
$item->delete();
}
}
return $item ?? true;
}
2025-11-29 11:05:02 +00:00
public function checkout(): static
{
$items = $this->items()
->with('purchasable')
->get();
if ($items->isEmpty()) {
throw new \Exception("Cart is empty");
}
// Create ProductPurchase for each cart item
foreach ($items as $item) {
$product = $item->purchasable;
$quantity = $item->quantity;
2025-12-09 08:42:59 +00:00
// Get booking dates from cart item directly (preferred) or from parameters (legacy)
$from = $item->from;
$until = $item->until;
if (!$from || !$until) {
if (($product->type === ProductType::BOOKING || $product->type === ProductType::POOL) && $item->parameters) {
$params = is_array($item->parameters) ? $item->parameters : (array) $item->parameters;
$from = $params['from'] ?? null;
$until = $params['until'] ?? null;
// Convert to Carbon instances if they're strings
if ($from && is_string($from)) {
$from = \Carbon\Carbon::parse($from);
}
if ($until && is_string($until)) {
$until = \Carbon\Carbon::parse($until);
}
}
}
// Handle pool products with booking single items
if ($product instanceof Product && $product->isPool()) {
// Check if pool with booking items requires timespan
if ($product->hasBookingSingleItems() && (!$from || !$until)) {
throw new \Exception("Pool product '{$product->name}' with booking items requires a timespan (from/until dates).");
}
// If pool has timespan and has booking single items, claim stock from single items
if ($from && $until && $product->hasBookingSingleItems()) {
try {
$claimedItems = $product->claimPoolStock(
$quantity,
$this,
$from,
$until,
"Checkout from cart {$this->id}"
);
// Store claimed items info in purchase meta
$item->updateMetaKey('claimed_single_items', array_map(fn($i) => $i->id, $claimedItems));
$item->save();
} catch (\Exception $e) {
throw new \Exception("Failed to checkout pool product '{$product->name}': " . $e->getMessage());
}
}
}
2025-11-29 11:05:02 +00:00
// Validate booking products have required dates
if ($product instanceof Product && $product->isBooking() && (!$from || !$until)) {
throw new \Exception("Booking product '{$product->name}' requires a timespan (from/until dates).");
}
2025-11-29 11:05:02 +00:00
$purchase = $this->customer->purchase(
$product->prices()->first(),
$quantity,
null,
$from,
$until
2025-11-29 11:05:02 +00:00
);
$purchase->update([
'cart_id' => $item->cart_id,
]);
// Remove item from cart
$item->update([
'purchase_id' => $purchase->id,
]);
}
$this->update([
'converted_at' => now(),
]);
return $this;
}
2025-11-21 10:49:41 +00:00
}