BFI cart stock, A exceptions
This commit is contained in:
parent
317b28af8a
commit
145c629786
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CartAlreadyConvertedException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Cart has already been converted/checked out.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CartDatesRequiredException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Both 'from' and 'until' dates must be provided together, or both omitted.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CartEmptyException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Cart is empty.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CartItemMissingInformationException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $productName, string $missingFields)
|
||||||
|
{
|
||||||
|
parent::__construct("Cart item '{$productName}' is missing required information: {$missingFields}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CartableInterfaceException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Item must implement the Cartable interface.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,11 @@ namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
class HasNoDefaultPriceException extends NotPurchasable
|
class HasNoDefaultPriceException extends NotPurchasable
|
||||||
{
|
{
|
||||||
|
public static function forProduct(string $productName): self
|
||||||
|
{
|
||||||
|
return new self("Product '{$productName}' has no default price.");
|
||||||
|
}
|
||||||
|
|
||||||
public static function multiplePricesNoDefault(string $productName, int $priceCount): self
|
public static function multiplePricesNoDefault(string $productName, int $priceCount): self
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidPricingStrategyException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $strategy)
|
||||||
|
{
|
||||||
|
parent::__construct("Invalid pricing strategy: {$strategy}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class NotPoolProductException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "This method is only for pool products.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class PoolHasNoItemsException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Pool product has no single items to claim.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class PriceCalculationException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $productName, ?int $pricePerDay = null, ?int $days = null)
|
||||||
|
{
|
||||||
|
$message = "Cart item price calculation resulted in null for '{$productName}'";
|
||||||
|
if ($pricePerDay !== null && $days !== null) {
|
||||||
|
$message .= " (pricePerDay: {$pricePerDay}, days: {$days})";
|
||||||
|
}
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ProductHasNoPriceException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $productName)
|
||||||
|
{
|
||||||
|
parent::__construct("Product '{$productName}' has no valid price.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ProductMissingAssociationException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Cannot sync price without associated product.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class StripeNotEnabledException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $message = "Stripe is not enabled.")
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class UnsupportedPaymentProviderException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $provider)
|
||||||
|
{
|
||||||
|
parent::__construct("Unsupported payment provider: {$provider}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,16 @@ 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\Enums\PurchaseStatus;
|
use Blax\Shop\Enums\PurchaseStatus;
|
||||||
|
use Blax\Shop\Exceptions\CartableInterfaceException;
|
||||||
|
use Blax\Shop\Exceptions\CartAlreadyConvertedException;
|
||||||
|
use Blax\Shop\Exceptions\CartDatesRequiredException;
|
||||||
|
use Blax\Shop\Exceptions\CartEmptyException;
|
||||||
|
use Blax\Shop\Exceptions\CartItemMissingInformationException;
|
||||||
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
||||||
use Blax\Shop\Exceptions\NotEnoughAvailableInTimespanException;
|
use Blax\Shop\Exceptions\NotEnoughAvailableInTimespanException;
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
use Blax\Shop\Exceptions\PriceCalculationException;
|
||||||
|
use Blax\Shop\Exceptions\ProductHasNoPriceException;
|
||||||
use Blax\Shop\Services\CartService;
|
use Blax\Shop\Services\CartService;
|
||||||
use Blax\Shop\Traits\ChecksIfBooking;
|
use Blax\Shop\Traits\ChecksIfBooking;
|
||||||
use Blax\Shop\Traits\HasBookingPriceCalculation;
|
use Blax\Shop\Traits\HasBookingPriceCalculation;
|
||||||
|
|
@ -216,39 +224,60 @@ class Cart extends Model
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setDates(
|
public function setDates(
|
||||||
\DateTimeInterface|string|int|float $from,
|
\DateTimeInterface|string|int|float|null $from,
|
||||||
\DateTimeInterface|string|int|float $until,
|
\DateTimeInterface|string|int|float|null $until,
|
||||||
bool $validateAvailability = true,
|
bool $validateAvailability = true,
|
||||||
bool $overwrite_item_dates = true
|
bool $overwrite_item_dates = true
|
||||||
): self {
|
): self {
|
||||||
// Parse string dates using Carbon
|
// Parse string dates using Carbon
|
||||||
if (is_string($from) || is_numeric($from)) {
|
if ($from !== null && (is_string($from) || is_numeric($from))) {
|
||||||
$from = Carbon::parse($from);
|
$from = Carbon::parse($from);
|
||||||
}
|
}
|
||||||
if (is_string($until) || is_numeric($until)) {
|
if ($until !== null && (is_string($until) || is_numeric($until))) {
|
||||||
$until = Carbon::parse($until);
|
$until = Carbon::parse($until);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($from >= $until) {
|
// Always update cart dates with provided values
|
||||||
throw new InvalidDateRangeException();
|
$updateData = [];
|
||||||
|
if ($from !== null) {
|
||||||
|
$updateData['from'] = $from;
|
||||||
|
}
|
||||||
|
if ($until !== null) {
|
||||||
|
$updateData['until'] = $until;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($updateData)) {
|
||||||
|
$this->update($updateData);
|
||||||
|
$this->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current dates (may include one from database if only one was updated)
|
||||||
|
$effectiveFrom = $from ?? $this->from;
|
||||||
|
$effectiveUntil = $until ?? $this->until;
|
||||||
|
|
||||||
|
// 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) {
|
if ($validateAvailability) {
|
||||||
// When overwriting item dates, validate against the new cart dates
|
// Validate against the correctly ordered dates
|
||||||
$this->validateDateAvailability($from, $until, $overwrite_item_dates);
|
$this->validateDateAvailability($calcFrom, $calcUntil, $overwrite_item_dates);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cart with from/until
|
// Update cart items with correctly ordered dates
|
||||||
$this->update([
|
|
||||||
'from' => $from,
|
|
||||||
'until' => $until,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update cart items with from/until
|
|
||||||
$this->applyDatesToItems(
|
$this->applyDatesToItems(
|
||||||
$validateAvailability,
|
$validateAvailability,
|
||||||
$overwrite_item_dates
|
$overwrite_item_dates,
|
||||||
|
$calcFrom,
|
||||||
|
$calcUntil
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->fresh();
|
return $this->fresh();
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +288,6 @@ class Cart extends Model
|
||||||
* @param \DateTimeInterface|string $from Start date (DateTimeInterface or parsable string)
|
* @param \DateTimeInterface|string $from Start date (DateTimeInterface or parsable string)
|
||||||
* @param bool $validateAvailability Whether to validate product availability for the timespan
|
* @param bool $validateAvailability Whether to validate product availability for the timespan
|
||||||
* @return $this
|
* @return $this
|
||||||
* @throws InvalidDateRangeException
|
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setFromDate(
|
public function setFromDate(
|
||||||
|
|
@ -271,15 +299,24 @@ class Cart extends Model
|
||||||
$from = Carbon::parse($from);
|
$from = Carbon::parse($from);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->until && $from >= $this->until) {
|
// Always update the from date
|
||||||
throw new InvalidDateRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($validateAvailability && $this->until) {
|
|
||||||
$this->validateDateAvailability($from, $this->until);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->update(['from' => $from]);
|
$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();
|
return $this->fresh();
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +327,6 @@ class Cart extends Model
|
||||||
* @param \DateTimeInterface|string $until End date (DateTimeInterface or parsable string)
|
* @param \DateTimeInterface|string $until End date (DateTimeInterface or parsable string)
|
||||||
* @param bool $validateAvailability Whether to validate product availability for the timespan
|
* @param bool $validateAvailability Whether to validate product availability for the timespan
|
||||||
* @return $this
|
* @return $this
|
||||||
* @throws InvalidDateRangeException
|
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setUntilDate(\DateTimeInterface|string|int|float $until, bool $validateAvailability = true): self
|
public function setUntilDate(\DateTimeInterface|string|int|float $until, bool $validateAvailability = true): self
|
||||||
|
|
@ -300,15 +336,24 @@ class Cart extends Model
|
||||||
$until = Carbon::parse($until);
|
$until = Carbon::parse($until);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->from && $this->from >= $until) {
|
// Always update the until date
|
||||||
throw new InvalidDateRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($validateAvailability && $this->from) {
|
|
||||||
$this->validateDateAvailability($this->from, $until);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->update(['until' => $until]);
|
$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();
|
return $this->fresh();
|
||||||
}
|
}
|
||||||
|
|
@ -318,12 +363,22 @@ class Cart extends Model
|
||||||
*
|
*
|
||||||
* @param bool $validateAvailability Whether to validate product availability for the timespan
|
* @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 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
|
* @return $this
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function applyDatesToItems(bool $validateAvailability = true, bool $overwrite = false): self
|
public function applyDatesToItems(
|
||||||
{
|
bool $validateAvailability = true,
|
||||||
if (!$this->from || !$this->until) {
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,23 +393,23 @@ class Cart extends Model
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fromDate = $shouldApplyFrom ? $this->from : $item->from;
|
$itemFrom = $shouldApplyFrom ? $fromDate : $item->from;
|
||||||
$untilDate = $shouldApplyUntil ? $this->until : $item->until;
|
$itemUntil = $shouldApplyUntil ? $untilDate : $item->until;
|
||||||
|
|
||||||
if ($validateAvailability) {
|
if ($validateAvailability) {
|
||||||
$product = $item->purchasable;
|
$product = $item->purchasable;
|
||||||
if ($product && !$product->isAvailableForBooking($fromDate, $untilDate, $item->quantity)) {
|
if ($product && !$product->isAvailableForBooking($itemFrom, $itemUntil, $item->quantity)) {
|
||||||
throw new NotEnoughAvailableInTimespanException(
|
throw new NotEnoughAvailableInTimespanException(
|
||||||
productName: $product->name ?? 'Product',
|
productName: $product->name ?? 'Product',
|
||||||
requested: $item->quantity,
|
requested: $item->quantity,
|
||||||
available: 0, // Could calculate actual available amount
|
available: 0, // Could calculate actual available amount
|
||||||
from: $fromDate,
|
from: $itemFrom,
|
||||||
until: $untilDate
|
until: $itemUntil
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$item->updateDates($fromDate, $untilDate);
|
$item->updateDates($itemFrom, $itemUntil);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -493,7 +548,7 @@ class Cart extends Model
|
||||||
): CartItem {
|
): CartItem {
|
||||||
// $cartable must implement Cartable
|
// $cartable must implement Cartable
|
||||||
if (! $cartable instanceof 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
|
// Extract dates from parameters if not provided directly
|
||||||
|
|
@ -511,14 +566,14 @@ class Cart extends Model
|
||||||
if ($from && $until) {
|
if ($from && $until) {
|
||||||
$available = $cartable->getPoolMaxQuantity($from, $until);
|
$available = $cartable->getPoolMaxQuantity($from, $until);
|
||||||
if ($available !== PHP_INT_MAX && $quantity > $available) {
|
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}"
|
"Pool product '{$cartable->name}' has only {$available} items available for the requested period. Requested: {$quantity}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$available = $cartable->getPoolMaxQuantity();
|
$available = $cartable->getPoolMaxQuantity();
|
||||||
if ($available !== PHP_INT_MAX && $quantity > $available) {
|
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}"
|
"Pool product '{$cartable->name}' has only {$available} items available. Requested: {$quantity}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -541,12 +596,12 @@ class Cart extends Model
|
||||||
if ($from && $until) {
|
if ($from && $until) {
|
||||||
// Validate from is before until
|
// Validate from is before until
|
||||||
if ($from >= $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
|
// Check booking product availability if dates are provided
|
||||||
if ($cartable->isBooking() && !$cartable->isPool() && !$cartable->isAvailableForBooking($from, $until, $quantity)) {
|
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')})."
|
"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);
|
$maxQuantity = $cartable->getPoolMaxQuantity($from, $until);
|
||||||
// Only validate if pool has limited availability AND quantity exceeds it
|
// Only validate if pool has limited availability AND quantity exceeds it
|
||||||
if ($maxQuantity !== PHP_INT_MAX && $quantity > $maxQuantity) {
|
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}"
|
"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) {
|
} elseif ($from || $until) {
|
||||||
// If only one date is provided, it's an error
|
// 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 {
|
} else {
|
||||||
// Even without dates, check pool quantity limits
|
// Even without dates, check pool quantity limits
|
||||||
if ($cartable->isPool()) {
|
if ($cartable->isPool()) {
|
||||||
|
|
@ -580,7 +635,7 @@ class Cart extends Model
|
||||||
$totalQuantity = $currentQuantityInCart + $quantity;
|
$totalQuantity = $currentQuantityInCart + $quantity;
|
||||||
|
|
||||||
if ($totalQuantity > $maxQuantity) {
|
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}"
|
"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
|
// For pool products, throw specific error when neither pool nor single items have prices
|
||||||
throw \Blax\Shop\Exceptions\HasNoPriceException::poolProductNoPriceAndNoSingleItemPrices($cartable->name);
|
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
|
// Calculate days if booking dates provided
|
||||||
|
|
@ -714,7 +769,7 @@ class Cart extends Model
|
||||||
|
|
||||||
// Defensive check - ensure pricePerUnit is not null
|
// Defensive check - ensure pricePerUnit is not null
|
||||||
if ($pricePerUnit === 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
|
// 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
|
// Check if cart is already converted
|
||||||
if ($this->isConverted()) {
|
if ($this->isConverted()) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Exception("Cart has already been converted/checked out");
|
throw new CartAlreadyConvertedException();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -865,7 +920,7 @@ class Cart extends Model
|
||||||
|
|
||||||
if ($items->isEmpty()) {
|
if ($items->isEmpty()) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Exception("Cart is empty");
|
throw new CartEmptyException();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -880,7 +935,7 @@ class Cart extends Model
|
||||||
$missingFields = implode(', ', array_keys($adjustments));
|
$missingFields = implode(', ', array_keys($adjustments));
|
||||||
|
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Exception("Cart item '{$productName}' is missing required information: {$missingFields}");
|
throw new CartItemMissingInformationException($productName, $missingFields);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -895,8 +950,9 @@ class Cart extends Model
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$from = $item->from;
|
// Use effective dates (item-specific or cart fallback)
|
||||||
$until = $item->until;
|
$from = $item->getEffectiveFromDate();
|
||||||
|
$until = $item->getEffectiveUntilDate();
|
||||||
|
|
||||||
// For pool products, check pool availability
|
// For pool products, check pool availability
|
||||||
if ($product->isPool()) {
|
if ($product->isPool()) {
|
||||||
|
|
@ -915,7 +971,7 @@ class Cart extends Model
|
||||||
|
|
||||||
if ($available !== PHP_INT_MAX && $totalInCart > $available) {
|
if ($available !== PHP_INT_MAX && $totalInCart > $available) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Blax\Shop\Exceptions\NotEnoughStockException(
|
throw new NotEnoughStockException(
|
||||||
"Pool product '{$product->name}' has only {$available} items available for the period " .
|
"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}"
|
"{$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 ($available !== PHP_INT_MAX && $totalInCart > $available) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Blax\Shop\Exceptions\NotEnoughStockException(
|
throw new NotEnoughStockException(
|
||||||
"Pool product '{$product->name}' has only {$available} items available. Cart has: {$totalInCart}"
|
"Pool product '{$product->name}' has only {$available} items available. Cart has: {$totalInCart}"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -947,7 +1003,7 @@ class Cart extends Model
|
||||||
if ($from && $until) {
|
if ($from && $until) {
|
||||||
if (!$product->isAvailableForBooking($from, $until, $item->quantity)) {
|
if (!$product->isAvailableForBooking($from, $until, $item->quantity)) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Blax\Shop\Exceptions\NotEnoughStockException(
|
throw new NotEnoughStockException(
|
||||||
"Booking product '{$product->name}' is not available for the period " .
|
"Booking product '{$product->name}' is not available for the period " .
|
||||||
"{$from->format('Y-m-d')} to {$until->format('Y-m-d')}. Requested: {$item->quantity}"
|
"{$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();
|
$available = $product->getAvailableStock();
|
||||||
if ($item->quantity > $available) {
|
if ($item->quantity > $available) {
|
||||||
if ($throws) {
|
if ($throws) {
|
||||||
throw new \Blax\Shop\Exceptions\NotEnoughStockException(
|
throw new NotEnoughStockException(
|
||||||
"Product '{$product->name}' has only {$available} items in stock. Requested: {$item->quantity}"
|
"Product '{$product->name}' has only {$available} items in stock. Requested: {$item->quantity}"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
return true;
|
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()
|
$overlappingClaims = $this->stocks()
|
||||||
->where('type', StockType::CLAIMED->value)
|
->where('type', StockType::CLAIMED->value)
|
||||||
->where('status', StockStatus::PENDING->value)
|
->where('status', StockStatus::PENDING->value)
|
||||||
|
|
@ -362,7 +362,18 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
})
|
})
|
||||||
->sum('quantity');
|
->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;
|
return $availableStock >= $quantity;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Blax\Shop\Services\PaymentProvider;
|
namespace Blax\Shop\Services\PaymentProvider;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\UnsupportedPaymentProviderException;
|
||||||
use Blax\Shop\Models\PaymentMethod;
|
use Blax\Shop\Models\PaymentMethod;
|
||||||
use Blax\Shop\Models\PaymentProviderIdentity;
|
use Blax\Shop\Models\PaymentProviderIdentity;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -65,7 +66,7 @@ class PaymentProviderService
|
||||||
return $identity;
|
return $identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \InvalidArgumentException("Unsupported payment provider: {$provider}");
|
throw new UnsupportedPaymentProviderException($provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,7 +113,7 @@ class PaymentProviderService
|
||||||
return $paymentMethod;
|
return $paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \InvalidArgumentException("Unsupported payment provider: {$identity->provider_name}");
|
throw new UnsupportedPaymentProviderException($identity->provider_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Services;
|
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\Product;
|
||||||
use Blax\Shop\Models\ProductPrice;
|
use Blax\Shop\Models\ProductPrice;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
@ -27,7 +30,7 @@ class StripeSyncService
|
||||||
public function syncProduct(Product $product): string
|
public function syncProduct(Product $product): string
|
||||||
{
|
{
|
||||||
if (!config('shop.stripe.enabled')) {
|
if (!config('shop.stripe.enabled')) {
|
||||||
throw new \Exception('Stripe is not enabled');
|
throw new StripeNotEnabledException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if product already has a Stripe ID
|
// Check if product already has a Stripe ID
|
||||||
|
|
@ -89,7 +92,7 @@ class StripeSyncService
|
||||||
public function syncPrice(ProductPrice $price, ?Product $product = null): string
|
public function syncPrice(ProductPrice $price, ?Product $product = null): string
|
||||||
{
|
{
|
||||||
if (!config('shop.stripe.enabled')) {
|
if (!config('shop.stripe.enabled')) {
|
||||||
throw new \Exception('Stripe is not enabled');
|
throw new StripeNotEnabledException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the product if not provided
|
// Get the product if not provided
|
||||||
|
|
@ -98,7 +101,7 @@ class StripeSyncService
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
throw new \Exception('Cannot sync price without associated product');
|
throw new ProductMissingAssociationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure product is synced to Stripe
|
// Ensure product is synced to Stripe
|
||||||
|
|
@ -184,7 +187,7 @@ class StripeSyncService
|
||||||
$defaultPrice = $product->defaultPrice()->first();
|
$defaultPrice = $product->defaultPrice()->first();
|
||||||
|
|
||||||
if (!$defaultPrice) {
|
if (!$defaultPrice) {
|
||||||
throw new \Exception("Product '{$product->name}' has no default price");
|
throw HasNoDefaultPriceException::forProduct($product->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stripePriceId = $this->syncPrice($defaultPrice, $product);
|
$stripePriceId = $this->syncPrice($defaultPrice, $product);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Blax\Shop\Traits;
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Blax\Shop\Exceptions\NotPurchasable;
|
use Blax\Shop\Exceptions\NotPurchasable;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\CartItem;
|
use Blax\Shop\Models\CartItem;
|
||||||
|
|
@ -46,31 +47,41 @@ trait HasCart
|
||||||
/**
|
/**
|
||||||
* Add product to cart
|
* 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 Product|ProductPrice $product_or_price
|
||||||
* @param int $quantity
|
* @param int $quantity
|
||||||
* @param array $options
|
* @param array $parameters Optional parameters including 'from' and 'until' for booking dates
|
||||||
* @return CartItem
|
* @return CartItem
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem
|
public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem
|
||||||
{
|
{
|
||||||
if ($product_or_price instanceof ProductPrice) {
|
$product = $product_or_price instanceof ProductPrice
|
||||||
$product = $product_or_price->purchasable;
|
? $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);
|
$product->claimStock($quantity);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($product_or_price instanceof Product) {
|
|
||||||
$product_or_price->claimStock($quantity);
|
|
||||||
|
|
||||||
// Skip default price validation for pool products without direct prices
|
// Skip default price validation for pool products without direct prices
|
||||||
// (they inherit pricing from single items and are validated in validatePricing())
|
// (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) {
|
if (!$isPoolWithInheritedPricing) {
|
||||||
$default_prices = $product_or_price->defaultPrice()->count();
|
$default_prices = $product->defaultPrice()->count();
|
||||||
|
|
||||||
if ($default_prices === 0) {
|
if ($default_prices === 0) {
|
||||||
throw new NotPurchasable("Product has no default price");
|
throw new NotPurchasable("Product has no default price");
|
||||||
|
|
@ -103,7 +114,7 @@ trait HasCart
|
||||||
|
|
||||||
// Validate stock
|
// Validate stock
|
||||||
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
||||||
throw new \Exception("Insufficient stock available");
|
throw new NotEnoughStockException("Insufficient stock available");
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta = (array) $cartItem->meta;
|
$meta = (array) $cartItem->meta;
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ trait HasShoppingCapabilities
|
||||||
throw new \Exception("Booking products require 'from' and 'until' dates");
|
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)) {
|
if (!$product->decreaseStock($quantity, $isBooking ? $until : null)) {
|
||||||
throw new \Exception("Unable to decrease stock");
|
throw new \Exception("Unable to decrease stock");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ use Blax\Shop\Enums\PricingStrategy;
|
||||||
use Blax\Shop\Enums\StockStatus;
|
use Blax\Shop\Enums\StockStatus;
|
||||||
use Blax\Shop\Enums\StockType;
|
use Blax\Shop\Enums\StockType;
|
||||||
use Blax\Shop\Exceptions\InvalidPoolConfigurationException;
|
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
|
trait MayBePoolProduct
|
||||||
{
|
{
|
||||||
|
|
@ -126,13 +130,13 @@ trait MayBePoolProduct
|
||||||
?string $note = null
|
?string $note = null
|
||||||
): array {
|
): array {
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$singleItems = $this->singleProducts;
|
$singleItems = $this->singleProducts;
|
||||||
|
|
||||||
if ($singleItems->isEmpty()) {
|
if ($singleItems->isEmpty()) {
|
||||||
throw new \Exception('Pool product has no single items to claim');
|
throw new PoolHasNoItemsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get pricing strategy
|
// Get pricing strategy
|
||||||
|
|
@ -159,7 +163,7 @@ trait MayBePoolProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($availableItems) < $quantity) {
|
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
|
// Sort by pricing strategy
|
||||||
|
|
@ -191,7 +195,7 @@ trait MayBePoolProduct
|
||||||
public function releasePoolStock($reference): int
|
public function releasePoolStock($reference): int
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$singleItems = $this->singleProducts;
|
$singleItems = $this->singleProducts;
|
||||||
|
|
@ -564,14 +568,14 @@ trait MayBePoolProduct
|
||||||
public function setPoolPricingStrategy(string|PricingStrategy $strategy): void
|
public function setPoolPricingStrategy(string|PricingStrategy $strategy): void
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle both string and enum inputs
|
// Handle both string and enum inputs
|
||||||
if (is_string($strategy)) {
|
if (is_string($strategy)) {
|
||||||
$strategyEnum = PricingStrategy::tryFrom($strategy);
|
$strategyEnum = PricingStrategy::tryFrom($strategy);
|
||||||
if (!$strategyEnum) {
|
if (!$strategyEnum) {
|
||||||
throw new \InvalidArgumentException("Invalid pricing strategy: {$strategy}");
|
throw new InvalidPricingStrategyException($strategy);
|
||||||
}
|
}
|
||||||
$strategy = $strategyEnum;
|
$strategy = $strategyEnum;
|
||||||
}
|
}
|
||||||
|
|
@ -874,7 +878,7 @@ trait MayBePoolProduct
|
||||||
public function attachSingleItems(array|int|string $singleItemIds, array $attributes = []): void
|
public function attachSingleItems(array|int|string $singleItemIds, array $attributes = []): void
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$ids = is_array($singleItemIds) ? $singleItemIds : [$singleItemIds];
|
$ids = is_array($singleItemIds) ? $singleItemIds : [$singleItemIds];
|
||||||
|
|
@ -1028,7 +1032,7 @@ trait MayBePoolProduct
|
||||||
public function getPoolAvailabilityCalendar($startDate, $endDate, int $quantity = 1): array
|
public function getPoolAvailabilityCalendar($startDate, $endDate, int $quantity = 1): array
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
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);
|
$start = $startDate instanceof \DateTimeInterface ? $startDate : \Carbon\Carbon::parse($startDate);
|
||||||
|
|
@ -1072,7 +1076,7 @@ trait MayBePoolProduct
|
||||||
public function getSingleItemsAvailability($from = null, $until = null): array
|
public function getSingleItemsAvailability($from = null, $until = null): array
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$singleItems = $this->singleProducts;
|
$singleItems = $this->singleProducts;
|
||||||
|
|
@ -1131,7 +1135,7 @@ trait MayBePoolProduct
|
||||||
public function isPoolAvailable(\DateTimeInterface $from, \DateTimeInterface $until, int $quantity = 1): bool
|
public function isPoolAvailable(\DateTimeInterface $from, \DateTimeInterface $until, int $quantity = 1): bool
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
throw new \Exception('This method is only for pool products');
|
throw new NotPoolProductException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$maxQuantity = $this->getPoolMaxQuantity($from, $until);
|
$maxQuantity = $this->getPoolMaxQuantity($from, $until);
|
||||||
|
|
@ -1157,7 +1161,7 @@ trait MayBePoolProduct
|
||||||
public function getPoolAvailablePeriods($startDate, $endDate, int $quantity = 1, int $minConsecutiveDays = 1): array
|
public function getPoolAvailablePeriods($startDate, $endDate, int $quantity = 1, int $minConsecutiveDays = 1): array
|
||||||
{
|
{
|
||||||
if (!$this->isPool()) {
|
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);
|
$start = $startDate instanceof \DateTimeInterface ? $startDate : \Carbon\Carbon::parse($startDate);
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,19 @@ class CartDateManagementTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @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();
|
$cart = Cart::factory()->create();
|
||||||
$from = Carbon::now()->addDays(3);
|
$from = Carbon::now()->addDays(3);
|
||||||
$until = Carbon::now()->addDays(1);
|
$until = Carbon::now()->addDays(1);
|
||||||
|
|
||||||
$this->expectException(InvalidDateRangeException::class);
|
// Dates are stored as provided (backwards)
|
||||||
$cart->setDates($from, $until, validateAvailability: false);
|
$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 */
|
/** @test */
|
||||||
|
|
@ -64,25 +69,37 @@ class CartDateManagementTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @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([
|
$cart = Cart::factory()->create([
|
||||||
'until' => Carbon::now()->addDays(2),
|
'until' => $until,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->expectException(InvalidDateRangeException::class);
|
$from = Carbon::now()->addDays(3);
|
||||||
$cart->setFromDate(Carbon::now()->addDays(3), validateAvailability: false);
|
$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 */
|
/** @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([
|
$cart = Cart::factory()->create([
|
||||||
'from' => Carbon::now()->addDays(3),
|
'from' => $from,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->expectException(InvalidDateRangeException::class);
|
$until = Carbon::now()->addDays(2);
|
||||||
$cart->setUntilDate(Carbon::now()->addDays(2), validateAvailability: false);
|
$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 */
|
/** @test */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue