2025-11-21 10:49:41 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Blax\Shop\Models;
|
|
|
|
|
|
2025-11-23 14:07:12 +00:00
|
|
|
use Blax\Shop\Contracts\Cartable;
|
2025-12-03 12:59:01 +00:00
|
|
|
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 = [
|
2025-12-03 12:59:01 +00:00
|
|
|
'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-11-23 14:07:12 +00:00
|
|
|
|
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
|
|
|
|
|
*/
|
2025-11-23 14:07:12 +00:00
|
|
|
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 {
|
2025-11-23 14:07:12 +00:00
|
|
|
// $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
|
2025-11-23 14:07:12 +00:00
|
|
|
$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,
|
2025-11-23 14:07:12 +00:00
|
|
|
'parameters' => $parameters,
|
|
|
|
|
]);
|
|
|
|
|
|
2025-12-09 08:42:59 +00:00
|
|
|
return $cartItem->fresh();
|
2025-11-23 14:07:12 +00:00
|
|
|
}
|
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
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
// Extract booking dates from parameters if this is a booking product
|
|
|
|
|
$from = null;
|
|
|
|
|
$until = null;
|
|
|
|
|
if ($product->type === ProductType::BOOKING && $item->parameters) {
|
|
|
|
|
$params = is_array($item->parameters) ? $item->parameters : (array) $item->parameters;
|
|
|
|
|
$from = $params['from'] ?? null;
|
|
|
|
|
$until = $params['until'] ?? null;
|
2025-12-09 08:42:59 +00:00
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-29 11:05:02 +00:00
|
|
|
|
|
|
|
|
$purchase = $this->customer->purchase(
|
|
|
|
|
$product->prices()->first(),
|
2025-12-03 12:59:01 +00:00
|
|
|
$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
|
|
|
}
|