'integer', 'price' => 'decimal:2', 'regular_price' => 'decimal:2', 'subtotal' => 'decimal:2', 'parameters' => 'array', 'meta' => 'array', 'from' => 'datetime', 'until' => 'datetime', ]; public function __construct(array $attributes = []) { parent::__construct($attributes); $this->table = config('shop.tables.cart_items', 'cart_items'); } protected static function boot() { parent::boot(); // Auto-calculate subtotal before saving static::creating(function ($cartItem) { if (!isset($cartItem->subtotal)) { $cartItem->subtotal = $cartItem->quantity * $cartItem->price; } }); static::updating(function ($cartItem) { if ($cartItem->isDirty(['quantity', 'price'])) { $cartItem->subtotal = $cartItem->quantity * $cartItem->price; } }); } public function cart(): BelongsTo { return $this->belongsTo(config('shop.models.cart'), 'cart_id'); } public function purchasable() { return $this->morphTo('purchasable'); } public function purchase() { return $this->hasOne( config('shop.models.product_purchase', ProductPurchase::class), 'id', 'purchase_id' ); } public function product(): BelongsTo|null { if ($this->purchasable_type === config('shop.models.product', Product::class)) { return $this->belongsTo(config('shop.models.product'), 'purchasable_id'); } return null; } public function getSubtotal(): float { return $this->quantity * $this->price; } public function scopeForCart($query, $cartId) { return $query->where('cart_id', $cartId); } public function scopeForProduct($query, $productId) { return $query->where('product_id', $productId); } /** * Get required adjustments for this cart item before checkout. * * Returns an array of fields that need to be set, with suggested field names. * For booking products and pools with booking items, dates are required. * * This method is useful for: * - Validating cart items before checkout * - Displaying missing information to users * - Checking if a cart item needs additional user input * * Example usage: * ```php * // Check if cart item needs adjustments * $adjustments = $cartItem->requiredAdjustments(); * * if (!empty($adjustments)) { * // Item needs dates before checkout * // $adjustments = ['from' => 'datetime', 'until' => 'datetime'] * echo "Please select booking dates"; * } * * // Check all cart items before checkout * foreach ($cart->items as $item) { * $required = $item->requiredAdjustments(); * if (!empty($required)) { * // Handle missing information * } * } * ``` * * @return array Array of required field adjustments, e.g., ['from' => 'datetime', 'until' => 'datetime'] */ public function requiredAdjustments(): array { $adjustments = []; // Only check if purchasable is a Product if ($this->purchasable_type !== config('shop.models.product', Product::class)) { return $adjustments; } $product = $this->purchasable; if (!$product) { return $adjustments; } // Check if dates are required (for booking products or pools with booking items) $requiresDates = $product->isBooking() || ($product->isPool() && $product->hasBookingSingleItems()); if ($requiresDates) { if (is_null($this->from)) { $adjustments['from'] = 'datetime'; } if (is_null($this->until)) { $adjustments['until'] = 'datetime'; } } return $adjustments; } /** * Update the booking dates for this cart item. * Automatically recalculates price based on the new date range. * * @param \DateTimeInterface $from Start date * @param \DateTimeInterface $until End date * @return $this * @throws \Exception If dates are invalid */ public function updateDates(\DateTimeInterface $from, \DateTimeInterface $until): self { if ($from >= $until) { throw new \Exception("The 'from' date must be before the 'until' date."); } $product = $this->purchasable; if (!$product || !($product instanceof Product)) { throw new \Exception("Cannot update dates for non-product items."); } // Calculate days $days = max(1, $from->diff($until)->days); // Get current price per day $pricePerDay = $product->getCurrentPrice(); $regularPricePerDay = $product->getCurrentPrice(false) ?? $pricePerDay; // Calculate new prices $pricePerUnit = $pricePerDay * $days; $regularPricePerUnit = $regularPricePerDay * $days; $this->update([ 'from' => $from, 'until' => $until, 'price' => $pricePerUnit, 'regular_price' => $regularPricePerUnit, 'subtotal' => $pricePerUnit * $this->quantity, ]); return $this->fresh(); } /** * Set the 'from' date for this cart item. * * @param \DateTimeInterface $from Start date * @return $this */ public function setFromDate(\DateTimeInterface $from): self { if ($this->until && $from >= $this->until) { throw new \Exception("The 'from' date must be before the 'until' date."); } $this->update(['from' => $from]); // If both dates are now set, recalculate pricing if ($this->until) { return $this->updateDates($from, $this->until); } return $this->fresh(); } /** * Set the 'until' date for this cart item. * * @param \DateTimeInterface $until End date * @return $this */ public function setUntilDate(\DateTimeInterface $until): self { if ($this->from && $this->from >= $until) { throw new \Exception("The 'until' date must be after the 'from' date."); } $this->update(['until' => $until]); // If both dates are now set, recalculate pricing if ($this->from) { return $this->updateDates($this->from, $until); } return $this->fresh(); } }