From 145c629786c85f8d674cb1a4f3f1f0d9c61d0585 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Fri, 19 Dec 2025 14:26:57 +0100 Subject: [PATCH] BFI cart stock, A exceptions --- .../CartAlreadyConvertedException.php | 13 ++ src/Exceptions/CartDatesRequiredException.php | 13 ++ src/Exceptions/CartEmptyException.php | 13 ++ .../CartItemMissingInformationException.php | 13 ++ src/Exceptions/CartableInterfaceException.php | 13 ++ src/Exceptions/HasNoDefaultPriceException.php | 5 + .../InvalidPricingStrategyException.php | 13 ++ src/Exceptions/NotPoolProductException.php | 13 ++ src/Exceptions/PoolHasNoItemsException.php | 13 ++ src/Exceptions/PriceCalculationException.php | 17 ++ src/Exceptions/ProductHasNoPriceException.php | 13 ++ .../ProductMissingAssociationException.php | 13 ++ src/Exceptions/StripeNotEnabledException.php | 13 ++ .../UnsupportedPaymentProviderException.php | 13 ++ src/Models/Cart.php | 186 ++++++++++++------ src/Models/Product.php | 15 +- .../PaymentProviderService.php | 5 +- src/Services/StripeSyncService.php | 11 +- src/Traits/HasCart.php | 33 ++-- src/Traits/HasShoppingCapabilities.php | 2 +- src/Traits/MayBePoolProduct.php | 26 +-- tests/Feature/CartDateManagementTest.php | 37 +++- 22 files changed, 387 insertions(+), 106 deletions(-) create mode 100644 src/Exceptions/CartAlreadyConvertedException.php create mode 100644 src/Exceptions/CartDatesRequiredException.php create mode 100644 src/Exceptions/CartEmptyException.php create mode 100644 src/Exceptions/CartItemMissingInformationException.php create mode 100644 src/Exceptions/CartableInterfaceException.php create mode 100644 src/Exceptions/InvalidPricingStrategyException.php create mode 100644 src/Exceptions/NotPoolProductException.php create mode 100644 src/Exceptions/PoolHasNoItemsException.php create mode 100644 src/Exceptions/PriceCalculationException.php create mode 100644 src/Exceptions/ProductHasNoPriceException.php create mode 100644 src/Exceptions/ProductMissingAssociationException.php create mode 100644 src/Exceptions/StripeNotEnabledException.php create mode 100644 src/Exceptions/UnsupportedPaymentProviderException.php diff --git a/src/Exceptions/CartAlreadyConvertedException.php b/src/Exceptions/CartAlreadyConvertedException.php new file mode 100644 index 0000000..de5517f --- /dev/null +++ b/src/Exceptions/CartAlreadyConvertedException.php @@ -0,0 +1,13 @@ += $until) { - throw new InvalidDateRangeException(); + // Always update cart dates with provided values + $updateData = []; + if ($from !== null) { + $updateData['from'] = $from; + } + if ($until !== null) { + $updateData['until'] = $until; } - if ($validateAvailability) { - // When overwriting item dates, validate against the new cart dates - $this->validateDateAvailability($from, $until, $overwrite_item_dates); + if (!empty($updateData)) { + $this->update($updateData); + $this->refresh(); } - // Update cart with from/until - $this->update([ - 'from' => $from, - 'until' => $until, - ]); + // Get the current dates (may include one from database if only one was updated) + $effectiveFrom = $from ?? $this->from; + $effectiveUntil = $until ?? $this->until; - // Update cart items with from/until - $this->applyDatesToItems( - $validateAvailability, - $overwrite_item_dates - ); + // Only calculate/validate if BOTH dates are set + if ($effectiveFrom && $effectiveUntil) { + // For calculations, swap if dates are backwards + $calcFrom = $effectiveFrom; + $calcUntil = $effectiveUntil; + if ($effectiveFrom > $effectiveUntil) { + $calcFrom = $effectiveUntil; + $calcUntil = $effectiveFrom; + } + + if ($validateAvailability) { + // Validate against the correctly ordered dates + $this->validateDateAvailability($calcFrom, $calcUntil, $overwrite_item_dates); + } + + // Update cart items with correctly ordered dates + $this->applyDatesToItems( + $validateAvailability, + $overwrite_item_dates, + $calcFrom, + $calcUntil + ); + } return $this->fresh(); } @@ -259,7 +288,6 @@ class Cart extends Model * @param \DateTimeInterface|string $from Start date (DateTimeInterface or parsable string) * @param bool $validateAvailability Whether to validate product availability for the timespan * @return $this - * @throws InvalidDateRangeException * @throws NotEnoughAvailableInTimespanException */ public function setFromDate( @@ -271,15 +299,24 @@ class Cart extends Model $from = Carbon::parse($from); } - if ($this->until && $from >= $this->until) { - throw new InvalidDateRangeException(); - } - - if ($validateAvailability && $this->until) { - $this->validateDateAvailability($from, $this->until); - } - + // Always update the from date $this->update(['from' => $from]); + $this->refresh(); + + // Only calculate if both dates are set + if ($this->until) { + // For calculations, swap if dates are backwards + $calcFrom = $from; + $calcUntil = $this->until; + if ($from > $this->until) { + $calcFrom = $this->until; + $calcUntil = $from; + } + + if ($validateAvailability) { + $this->validateDateAvailability($calcFrom, $calcUntil); + } + } return $this->fresh(); } @@ -290,7 +327,6 @@ class Cart extends Model * @param \DateTimeInterface|string $until End date (DateTimeInterface or parsable string) * @param bool $validateAvailability Whether to validate product availability for the timespan * @return $this - * @throws InvalidDateRangeException * @throws NotEnoughAvailableInTimespanException */ public function setUntilDate(\DateTimeInterface|string|int|float $until, bool $validateAvailability = true): self @@ -300,15 +336,24 @@ class Cart extends Model $until = Carbon::parse($until); } - if ($this->from && $this->from >= $until) { - throw new InvalidDateRangeException(); - } - - if ($validateAvailability && $this->from) { - $this->validateDateAvailability($this->from, $until); - } - + // Always update the until date $this->update(['until' => $until]); + $this->refresh(); + + // Only calculate if both dates are set + if ($this->from) { + // For calculations, swap if dates are backwards + $calcFrom = $this->from; + $calcUntil = $until; + if ($this->from > $until) { + $calcFrom = $until; + $calcUntil = $this->from; + } + + if ($validateAvailability) { + $this->validateDateAvailability($calcFrom, $calcUntil); + } + } return $this->fresh(); } @@ -318,12 +363,22 @@ class Cart extends Model * * @param bool $validateAvailability Whether to validate product availability for the timespan * @param bool $overwrite If true, overwrites existing item dates. If false, only sets null fields. + * @param \DateTimeInterface|null $from Optional from date (uses cart's from if not provided) + * @param \DateTimeInterface|null $until Optional until date (uses cart's until if not provided) * @return $this * @throws NotEnoughAvailableInTimespanException */ - public function applyDatesToItems(bool $validateAvailability = true, bool $overwrite = false): self - { - if (!$this->from || !$this->until) { + public function applyDatesToItems( + bool $validateAvailability = true, + bool $overwrite = false, + ?\DateTimeInterface $from = null, + ?\DateTimeInterface $until = null + ): self { + // Use provided dates or fall back to cart dates + $fromDate = $from ?? $this->from; + $untilDate = $until ?? $this->until; + + if (!$fromDate || !$untilDate) { return $this; } @@ -338,23 +393,23 @@ class Cart extends Model continue; } - $fromDate = $shouldApplyFrom ? $this->from : $item->from; - $untilDate = $shouldApplyUntil ? $this->until : $item->until; + $itemFrom = $shouldApplyFrom ? $fromDate : $item->from; + $itemUntil = $shouldApplyUntil ? $untilDate : $item->until; if ($validateAvailability) { $product = $item->purchasable; - if ($product && !$product->isAvailableForBooking($fromDate, $untilDate, $item->quantity)) { + if ($product && !$product->isAvailableForBooking($itemFrom, $itemUntil, $item->quantity)) { throw new NotEnoughAvailableInTimespanException( productName: $product->name ?? 'Product', requested: $item->quantity, available: 0, // Could calculate actual available amount - from: $fromDate, - until: $untilDate + from: $itemFrom, + until: $itemUntil ); } } - $item->updateDates($fromDate, $untilDate); + $item->updateDates($itemFrom, $itemUntil); } } @@ -493,7 +548,7 @@ class Cart extends Model ): CartItem { // $cartable must implement Cartable if (! $cartable instanceof Cartable) { - throw new \Exception("Item must implement the Cartable interface."); + throw new CartableInterfaceException(); } // Extract dates from parameters if not provided directly @@ -511,14 +566,14 @@ class Cart extends Model if ($from && $until) { $available = $cartable->getPoolMaxQuantity($from, $until); if ($available !== PHP_INT_MAX && $quantity > $available) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$cartable->name}' has only {$available} items available for the requested period. Requested: {$quantity}" ); } } else { $available = $cartable->getPoolMaxQuantity(); if ($available !== PHP_INT_MAX && $quantity > $available) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$cartable->name}' has only {$available} items available. Requested: {$quantity}" ); } @@ -541,12 +596,12 @@ class Cart extends Model if ($from && $until) { // Validate from is before until if ($from >= $until) { - throw new \Exception("The 'from' date must be before the 'until' date. Got from: {$from->format('Y-m-d H:i:s')}, until: {$until->format('Y-m-d H:i:s')}"); + throw new InvalidDateRangeException("The 'from' date must be before the 'until' date. Got from: {$from->format('Y-m-d H:i:s')}, until: {$until->format('Y-m-d H:i:s')}"); } // Check booking product availability if dates are provided if ($cartable->isBooking() && !$cartable->isPool() && !$cartable->isAvailableForBooking($from, $until, $quantity)) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Product '{$cartable->name}' is not available for the requested period ({$from->format('Y-m-d')} to {$until->format('Y-m-d')})." ); } @@ -556,14 +611,14 @@ class Cart extends Model $maxQuantity = $cartable->getPoolMaxQuantity($from, $until); // Only validate if pool has limited availability AND quantity exceeds it if ($maxQuantity !== PHP_INT_MAX && $quantity > $maxQuantity) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$cartable->name}' has only {$maxQuantity} items available for the requested period ({$from->format('Y-m-d')} to {$until->format('Y-m-d')}). Requested: {$quantity}" ); } } } elseif ($from || $until) { // If only one date is provided, it's an error - throw new \Exception("Both 'from' and 'until' dates must be provided together, or both omitted."); + throw new CartDatesRequiredException(); } else { // Even without dates, check pool quantity limits if ($cartable->isPool()) { @@ -580,7 +635,7 @@ class Cart extends Model $totalQuantity = $currentQuantityInCart + $quantity; if ($totalQuantity > $maxQuantity) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$cartable->name}' has only {$maxQuantity} items available. Already in cart: {$currentQuantityInCart}, Requested: {$quantity}" ); } @@ -699,7 +754,7 @@ class Cart extends Model // For pool products, throw specific error when neither pool nor single items have prices throw \Blax\Shop\Exceptions\HasNoPriceException::poolProductNoPriceAndNoSingleItemPrices($cartable->name); } - throw new \Exception("Product '{$cartable->name}' has no valid price."); + throw new ProductHasNoPriceException($cartable->name); } // Calculate days if booking dates provided @@ -714,7 +769,7 @@ class Cart extends Model // Defensive check - ensure pricePerUnit is not null if ($pricePerUnit === null) { - throw new \Exception("Cart item price calculation resulted in null for '{$cartable->name}' (pricePerDay: {$pricePerDay}, days: {$days})"); + throw new PriceCalculationException($cartable->name, $pricePerDay, $days); } // Store the base unit_amount (price for 1 quantity, 1 day) in cents @@ -853,7 +908,7 @@ class Cart extends Model // Check if cart is already converted if ($this->isConverted()) { if ($throws) { - throw new \Exception("Cart has already been converted/checked out"); + throw new CartAlreadyConvertedException(); } else { return false; } @@ -865,7 +920,7 @@ class Cart extends Model if ($items->isEmpty()) { if ($throws) { - throw new \Exception("Cart is empty"); + throw new CartEmptyException(); } else { return false; } @@ -880,7 +935,7 @@ class Cart extends Model $missingFields = implode(', ', array_keys($adjustments)); if ($throws) { - throw new \Exception("Cart item '{$productName}' is missing required information: {$missingFields}"); + throw new CartItemMissingInformationException($productName, $missingFields); } else { return false; } @@ -895,8 +950,9 @@ class Cart extends Model continue; } - $from = $item->from; - $until = $item->until; + // Use effective dates (item-specific or cart fallback) + $from = $item->getEffectiveFromDate(); + $until = $item->getEffectiveUntilDate(); // For pool products, check pool availability if ($product->isPool()) { @@ -915,7 +971,7 @@ class Cart extends Model if ($available !== PHP_INT_MAX && $totalInCart > $available) { if ($throws) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$product->name}' has only {$available} items available for the period " . "{$from->format('Y-m-d')} to {$until->format('Y-m-d')}. Cart has: {$totalInCart}" ); @@ -934,7 +990,7 @@ class Cart extends Model if ($available !== PHP_INT_MAX && $totalInCart > $available) { if ($throws) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Pool product '{$product->name}' has only {$available} items available. Cart has: {$totalInCart}" ); } else { @@ -947,7 +1003,7 @@ class Cart extends Model if ($from && $until) { if (!$product->isAvailableForBooking($from, $until, $item->quantity)) { if ($throws) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Booking product '{$product->name}' is not available for the period " . "{$from->format('Y-m-d')} to {$until->format('Y-m-d')}. Requested: {$item->quantity}" ); @@ -961,7 +1017,7 @@ class Cart extends Model $available = $product->getAvailableStock(); if ($item->quantity > $available) { if ($throws) { - throw new \Blax\Shop\Exceptions\NotEnoughStockException( + throw new NotEnoughStockException( "Product '{$product->name}' has only {$available} items in stock. Requested: {$item->quantity}" ); } else { diff --git a/src/Models/Product.php b/src/Models/Product.php index 44979c8..151543e 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -336,7 +336,7 @@ class Product extends Model implements Purchasable, Cartable return true; } - // Get stock claims that overlap with the requested period + // Get stock claims (CLAIMED entries) that overlap with the requested period $overlappingClaims = $this->stocks() ->where('type', StockType::CLAIMED->value) ->where('status', StockStatus::PENDING->value) @@ -362,7 +362,18 @@ class Product extends Model implements Purchasable, Cartable }) ->sum('quantity'); - $availableStock = $this->getAvailableStock() - abs($overlappingClaims); + // Also get DECREASE entries with expires_at that overlap (from completed bookings) + // These are booking purchases that reduce stock during the booking period + $overlappingBookings = $this->stocks() + ->where('type', StockType::DECREASE->value) + ->where('status', StockStatus::COMPLETED->value) + ->whereNotNull('expires_at') + ->where('expires_at', '>', $from) // Booking hasn't ended before our period starts + ->sum('quantity'); + + // Use base stock and subtract all overlapping reservations + // Note: overlappingBookings is already negative (DECREASE entries), so we add it + $availableStock = $this->getAvailableStock() - abs($overlappingClaims) + $overlappingBookings; return $availableStock >= $quantity; } diff --git a/src/Services/PaymentProvider/PaymentProviderService.php b/src/Services/PaymentProvider/PaymentProviderService.php index e46273f..ce17862 100644 --- a/src/Services/PaymentProvider/PaymentProviderService.php +++ b/src/Services/PaymentProvider/PaymentProviderService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Blax\Shop\Services\PaymentProvider; +use Blax\Shop\Exceptions\UnsupportedPaymentProviderException; use Blax\Shop\Models\PaymentMethod; use Blax\Shop\Models\PaymentProviderIdentity; use Illuminate\Database\Eloquent\Model; @@ -65,7 +66,7 @@ class PaymentProviderService return $identity; } - throw new \InvalidArgumentException("Unsupported payment provider: {$provider}"); + throw new UnsupportedPaymentProviderException($provider); } /** @@ -112,7 +113,7 @@ class PaymentProviderService return $paymentMethod; } - throw new \InvalidArgumentException("Unsupported payment provider: {$identity->provider_name}"); + throw new UnsupportedPaymentProviderException($identity->provider_name); } /** diff --git a/src/Services/StripeSyncService.php b/src/Services/StripeSyncService.php index 2ae777d..1aa7162 100644 --- a/src/Services/StripeSyncService.php +++ b/src/Services/StripeSyncService.php @@ -2,6 +2,9 @@ namespace Blax\Shop\Services; +use Blax\Shop\Exceptions\HasNoDefaultPriceException; +use Blax\Shop\Exceptions\ProductMissingAssociationException; +use Blax\Shop\Exceptions\StripeNotEnabledException; use Blax\Shop\Models\Product; use Blax\Shop\Models\ProductPrice; use Illuminate\Support\Facades\Log; @@ -27,7 +30,7 @@ class StripeSyncService public function syncProduct(Product $product): string { if (!config('shop.stripe.enabled')) { - throw new \Exception('Stripe is not enabled'); + throw new StripeNotEnabledException(); } // Check if product already has a Stripe ID @@ -89,7 +92,7 @@ class StripeSyncService public function syncPrice(ProductPrice $price, ?Product $product = null): string { if (!config('shop.stripe.enabled')) { - throw new \Exception('Stripe is not enabled'); + throw new StripeNotEnabledException(); } // Get the product if not provided @@ -98,7 +101,7 @@ class StripeSyncService } if (!$product) { - throw new \Exception('Cannot sync price without associated product'); + throw new ProductMissingAssociationException(); } // Ensure product is synced to Stripe @@ -184,7 +187,7 @@ class StripeSyncService $defaultPrice = $product->defaultPrice()->first(); if (!$defaultPrice) { - throw new \Exception("Product '{$product->name}' has no default price"); + throw HasNoDefaultPriceException::forProduct($product->name); } $stripePriceId = $this->syncPrice($defaultPrice, $product); diff --git a/src/Traits/HasCart.php b/src/Traits/HasCart.php index f87ff4e..ac48020 100644 --- a/src/Traits/HasCart.php +++ b/src/Traits/HasCart.php @@ -3,6 +3,7 @@ namespace Blax\Shop\Traits; use Blax\Shop\Exceptions\MultiplePurchaseOptions; +use Blax\Shop\Exceptions\NotEnoughStockException; use Blax\Shop\Exceptions\NotPurchasable; use Blax\Shop\Models\Cart; use Blax\Shop\Models\CartItem; @@ -46,31 +47,41 @@ trait HasCart /** * Add product to cart * + * For booking and pool products, stock is NOT claimed at add-to-cart time. + * Instead, availability is validated and stock is claimed at checkout time. + * This allows adding items to cart for future dates even if currently unavailable. + * + * For regular products with manage_stock, stock is claimed immediately to + * prevent overselling. + * * @param Product|ProductPrice $product_or_price * @param int $quantity - * @param array $options + * @param array $parameters Optional parameters including 'from' and 'until' for booking dates * @return CartItem * @throws \Exception */ public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem { - if ($product_or_price instanceof ProductPrice) { - $product = $product_or_price->purchasable; + $product = $product_or_price instanceof ProductPrice + ? $product_or_price->purchasable + : $product_or_price; - if ($product instanceof Product) { + if ($product instanceof Product) { + // For booking/pool products, do NOT claim stock at add-to-cart time + // Stock will be validated and claimed at checkout based on the booking dates + $isBookingOrPool = $product->isBooking() || $product->isPool(); + + if (!$isBookingOrPool) { + // For regular products, claim stock immediately to prevent overselling $product->claimStock($quantity); } - } - - if ($product_or_price instanceof Product) { - $product_or_price->claimStock($quantity); // Skip default price validation for pool products without direct prices // (they inherit pricing from single items and are validated in validatePricing()) - $isPoolWithInheritedPricing = $product_or_price->isPool() && !$product_or_price->prices()->exists(); + $isPoolWithInheritedPricing = $product->isPool() && !$product->prices()->exists(); if (!$isPoolWithInheritedPricing) { - $default_prices = $product_or_price->defaultPrice()->count(); + $default_prices = $product->defaultPrice()->count(); if ($default_prices === 0) { throw new NotPurchasable("Product has no default price"); @@ -103,7 +114,7 @@ trait HasCart // Validate stock if ($product->manage_stock && $product->getAvailableStock() < $quantity) { - throw new \Exception("Insufficient stock available"); + throw new NotEnoughStockException("Insufficient stock available"); } $meta = (array) $cartItem->meta; diff --git a/src/Traits/HasShoppingCapabilities.php b/src/Traits/HasShoppingCapabilities.php index a26f1a7..6b506c6 100644 --- a/src/Traits/HasShoppingCapabilities.php +++ b/src/Traits/HasShoppingCapabilities.php @@ -110,7 +110,7 @@ trait HasShoppingCapabilities throw new \Exception("Booking products require 'from' and 'until' dates"); } - // Decrease stock (for bookings, pass the until date) + // Decrease stock (for bookings, pass the until date as expiry so stock returns after booking ends) if (!$product->decreaseStock($quantity, $isBooking ? $until : null)) { throw new \Exception("Unable to decrease stock"); } diff --git a/src/Traits/MayBePoolProduct.php b/src/Traits/MayBePoolProduct.php index ce164fb..7f7c6fa 100644 --- a/src/Traits/MayBePoolProduct.php +++ b/src/Traits/MayBePoolProduct.php @@ -8,6 +8,10 @@ use Blax\Shop\Enums\PricingStrategy; use Blax\Shop\Enums\StockStatus; use Blax\Shop\Enums\StockType; use Blax\Shop\Exceptions\InvalidPoolConfigurationException; +use Blax\Shop\Exceptions\InvalidPricingStrategyException; +use Blax\Shop\Exceptions\NotEnoughStockException; +use Blax\Shop\Exceptions\NotPoolProductException; +use Blax\Shop\Exceptions\PoolHasNoItemsException; trait MayBePoolProduct { @@ -126,13 +130,13 @@ trait MayBePoolProduct ?string $note = null ): array { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $singleItems = $this->singleProducts; if ($singleItems->isEmpty()) { - throw new \Exception('Pool product has no single items to claim'); + throw new PoolHasNoItemsException(); } // Get pricing strategy @@ -159,7 +163,7 @@ trait MayBePoolProduct } if (count($availableItems) < $quantity) { - throw new \Exception("Only " . count($availableItems) . " items available, but {$quantity} requested"); + throw new NotEnoughStockException("Only " . count($availableItems) . " items available, but {$quantity} requested"); } // Sort by pricing strategy @@ -191,7 +195,7 @@ trait MayBePoolProduct public function releasePoolStock($reference): int { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $singleItems = $this->singleProducts; @@ -564,14 +568,14 @@ trait MayBePoolProduct public function setPoolPricingStrategy(string|PricingStrategy $strategy): void { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } // Handle both string and enum inputs if (is_string($strategy)) { $strategyEnum = PricingStrategy::tryFrom($strategy); if (!$strategyEnum) { - throw new \InvalidArgumentException("Invalid pricing strategy: {$strategy}"); + throw new InvalidPricingStrategyException($strategy); } $strategy = $strategyEnum; } @@ -874,7 +878,7 @@ trait MayBePoolProduct public function attachSingleItems(array|int|string $singleItemIds, array $attributes = []): void { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $ids = is_array($singleItemIds) ? $singleItemIds : [$singleItemIds]; @@ -1028,7 +1032,7 @@ trait MayBePoolProduct public function getPoolAvailabilityCalendar($startDate, $endDate, int $quantity = 1): array { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $start = $startDate instanceof \DateTimeInterface ? $startDate : \Carbon\Carbon::parse($startDate); @@ -1072,7 +1076,7 @@ trait MayBePoolProduct public function getSingleItemsAvailability($from = null, $until = null): array { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $singleItems = $this->singleProducts; @@ -1131,7 +1135,7 @@ trait MayBePoolProduct public function isPoolAvailable(\DateTimeInterface $from, \DateTimeInterface $until, int $quantity = 1): bool { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $maxQuantity = $this->getPoolMaxQuantity($from, $until); @@ -1157,7 +1161,7 @@ trait MayBePoolProduct public function getPoolAvailablePeriods($startDate, $endDate, int $quantity = 1, int $minConsecutiveDays = 1): array { if (!$this->isPool()) { - throw new \Exception('This method is only for pool products'); + throw new NotPoolProductException(); } $start = $startDate instanceof \DateTimeInterface ? $startDate : \Carbon\Carbon::parse($startDate); diff --git a/tests/Feature/CartDateManagementTest.php b/tests/Feature/CartDateManagementTest.php index 278507f..4e59cca 100644 --- a/tests/Feature/CartDateManagementTest.php +++ b/tests/Feature/CartDateManagementTest.php @@ -29,14 +29,19 @@ class CartDateManagementTest extends TestCase } /** @test */ - public function it_throws_exception_when_from_date_is_after_until_date() + public function it_stores_dates_as_provided_even_if_backwards() { $cart = Cart::factory()->create(); $from = Carbon::now()->addDays(3); $until = Carbon::now()->addDays(1); - $this->expectException(InvalidDateRangeException::class); + // Dates are stored as provided (backwards) $cart->setDates($from, $until, validateAvailability: false); + + $cart->refresh(); + // Database stores the dates as provided + $this->assertEquals($from->toDateTimeString(), $cart->from->toDateTimeString()); + $this->assertEquals($until->toDateTimeString(), $cart->until->toDateTimeString()); } /** @test */ @@ -64,25 +69,37 @@ class CartDateManagementTest extends TestCase } /** @test */ - public function it_throws_exception_when_setting_from_date_after_existing_until_date() + public function it_stores_from_date_even_if_after_existing_until_date() { + $until = Carbon::now()->addDays(2); $cart = Cart::factory()->create([ - 'until' => Carbon::now()->addDays(2), + 'until' => $until, ]); - $this->expectException(InvalidDateRangeException::class); - $cart->setFromDate(Carbon::now()->addDays(3), validateAvailability: false); + $from = Carbon::now()->addDays(3); + $cart->setFromDate($from, validateAvailability: false); + + $cart->refresh(); + // Database stores the dates as provided (backwards order) + $this->assertEquals($from->toDateTimeString(), $cart->from->toDateTimeString()); + $this->assertEquals($until->toDateTimeString(), $cart->until->toDateTimeString()); } /** @test */ - public function it_throws_exception_when_setting_until_date_before_existing_from_date() + public function it_stores_until_date_even_if_before_existing_from_date() { + $from = Carbon::now()->addDays(3); $cart = Cart::factory()->create([ - 'from' => Carbon::now()->addDays(3), + 'from' => $from, ]); - $this->expectException(InvalidDateRangeException::class); - $cart->setUntilDate(Carbon::now()->addDays(2), validateAvailability: false); + $until = Carbon::now()->addDays(2); + $cart->setUntilDate($until, validateAvailability: false); + + $cart->refresh(); + // Database stores the dates as provided (backwards order) + $this->assertEquals($from->toDateTimeString(), $cart->from->toDateTimeString()); + $this->assertEquals($until->toDateTimeString(), $cart->until->toDateTimeString()); } /** @test */