BFI cart & tests
This commit is contained in:
parent
9d07523d78
commit
3d7a273946
|
|
@ -13,11 +13,35 @@ class ProductFactory extends Factory
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
$name = $this->faker->words(3, true);
|
// Generate realistic product names
|
||||||
|
$productTypes = [
|
||||||
|
'Laptop',
|
||||||
|
'Smartphone',
|
||||||
|
'Headphones',
|
||||||
|
'Camera',
|
||||||
|
'Tablet',
|
||||||
|
'Watch',
|
||||||
|
'Monitor',
|
||||||
|
'Keyboard',
|
||||||
|
'Mouse',
|
||||||
|
'Speaker',
|
||||||
|
'Charger',
|
||||||
|
'Cable',
|
||||||
|
'Case',
|
||||||
|
'Stand',
|
||||||
|
'Adapter'
|
||||||
|
];
|
||||||
|
|
||||||
|
$adjectives = ['Pro', 'Max', 'Plus', 'Ultra', 'Premium', 'Deluxe'];
|
||||||
|
|
||||||
|
$productType = $this->faker->randomElement($productTypes);
|
||||||
|
$adjective = $this->faker->optional(0.6)->randomElement($adjectives);
|
||||||
|
|
||||||
|
$name = $adjective ? "{$productType} {$adjective}" : $productType;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => ucfirst($name),
|
'name' => $name,
|
||||||
'slug' => Str::slug($name),
|
'slug' => Str::slug($name . '-' . $this->faker->unique()->numberBetween(1000, 9999)),
|
||||||
'sku' => strtoupper($this->faker->bothify('??-####')),
|
'sku' => strtoupper($this->faker->bothify('??-####')),
|
||||||
'type' => 'simple',
|
'type' => 'simple',
|
||||||
'status' => 'published',
|
'status' => 'published',
|
||||||
|
|
@ -62,12 +86,25 @@ class ProductFactory extends Factory
|
||||||
null|float $sale_unit_amount = null
|
null|float $sale_unit_amount = null
|
||||||
): static {
|
): static {
|
||||||
return $this->afterCreating(function (Product $product) use ($count, $unit_amount, $sale_unit_amount) {
|
return $this->afterCreating(function (Product $product) use ($count, $unit_amount, $sale_unit_amount) {
|
||||||
|
// Use realistic price range if not specified
|
||||||
|
$defaultPrice = $unit_amount ?? $this->faker->randomElement([
|
||||||
|
1999, // $19.99
|
||||||
|
2999, // $29.99
|
||||||
|
4999, // $49.99
|
||||||
|
7999, // $79.99
|
||||||
|
9999, // $99.99
|
||||||
|
14999, // $149.99
|
||||||
|
19999, // $199.99
|
||||||
|
29999, // $299.99
|
||||||
|
49999, // $499.99
|
||||||
|
]);
|
||||||
|
|
||||||
$prices = \Blax\Shop\Models\ProductPrice::factory()
|
$prices = \Blax\Shop\Models\ProductPrice::factory()
|
||||||
->count($count)
|
->count($count)
|
||||||
->create([
|
->create([
|
||||||
'purchasable_type' => get_class($product),
|
'purchasable_type' => get_class($product),
|
||||||
'purchasable_id' => $product->id,
|
'purchasable_id' => $product->id,
|
||||||
'unit_amount' => $unit_amount ?? $this->faker->randomFloat(2, 10, 1000),
|
'unit_amount' => $defaultPrice,
|
||||||
'sale_unit_amount' => $sale_unit_amount,
|
'sale_unit_amount' => $sale_unit_amount,
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,37 @@ class ProductPriceFactory extends Factory
|
||||||
public function definition()
|
public function definition()
|
||||||
{
|
{
|
||||||
$type = $this->faker->randomElement(['one_time', 'recurring']);
|
$type = $this->faker->randomElement(['one_time', 'recurring']);
|
||||||
$unit_amount = $this->faker->randomFloat(2, 100, 40000);
|
|
||||||
$sale_unit_amount = $this->faker->randomFloat(2, $unit_amount * 0.5, $unit_amount * 0.80);
|
// Realistic price points (in cents)
|
||||||
|
$realisticPrices = [
|
||||||
|
999, // $9.99
|
||||||
|
1499, // $14.99
|
||||||
|
1999, // $19.99
|
||||||
|
2499, // $24.99
|
||||||
|
2999, // $29.99
|
||||||
|
3999, // $39.99
|
||||||
|
4999, // $49.99
|
||||||
|
5999, // $59.99
|
||||||
|
7999, // $79.99
|
||||||
|
9999, // $99.99
|
||||||
|
12999, // $129.99
|
||||||
|
14999, // $149.99
|
||||||
|
19999, // $199.99
|
||||||
|
24999, // $249.99
|
||||||
|
29999, // $299.99
|
||||||
|
];
|
||||||
|
|
||||||
|
$unit_amount = $this->faker->randomElement($realisticPrices);
|
||||||
|
$sale_unit_amount = $this->faker->optional(0.3)->passthrough(
|
||||||
|
intval($unit_amount * $this->faker->randomFloat(2, 0.7, 0.9))
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'billing_scheme' => $this->faker->randomElement(['per_unit', 'tiered']),
|
'billing_scheme' => $this->faker->randomElement(['per_unit', 'tiered']),
|
||||||
'unit_amount' => $this->faker->randomFloat(2, 1, 1000),
|
'unit_amount' => $unit_amount,
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
'is_default' => false,
|
'is_default' => false,
|
||||||
'unit_amount' => $unit_amount,
|
|
||||||
'sale_unit_amount' => $sale_unit_amount,
|
'sale_unit_amount' => $sale_unit_amount,
|
||||||
'interval' => $type === 'recurring' ? $this->faker->randomElement(['day', 'week', 'month', 'quarter', 'year']) : null,
|
'interval' => $type === 'recurring' ? $this->faker->randomElement(['day', 'week', 'month', 'quarter', 'year']) : null,
|
||||||
'interval_count' => $type === 'recurring' ? $this->faker->numberBetween(1, 12) : null,
|
'interval_count' => $type === 'recurring' ? $this->faker->numberBetween(1, 12) : null,
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ class HasNoPriceException extends NotPurchasable
|
||||||
public static function poolProductNoPriceAndNoSingleItemPrices(string $productName): self
|
public static function poolProductNoPriceAndNoSingleItemPrices(string $productName): self
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
"Pool product '{$productName}' has no pricing configured.\n\n" .
|
"Cannot add pool product '{$productName}' to cart: No pricing available.\n\n" .
|
||||||
"Pool products need pricing through one of two methods:\n\n" .
|
"Pool products can be priced in two ways:\n\n" .
|
||||||
"Option 1: Direct pool pricing (Recommended)\n" .
|
"Option 1: Direct pool pricing\n" .
|
||||||
"ProductPrice::create([\n" .
|
"ProductPrice::create([\n" .
|
||||||
" 'purchasable_id' => \$poolProduct->id,\n" .
|
" 'purchasable_id' => \$poolProduct->id,\n" .
|
||||||
" 'purchasable_type' => Product::class,\n" .
|
" 'purchasable_type' => Product::class,\n" .
|
||||||
|
|
@ -39,7 +39,7 @@ class HasNoPriceException extends NotPurchasable
|
||||||
" 'currency' => 'USD',\n" .
|
" 'currency' => 'USD',\n" .
|
||||||
" 'is_default' => true,\n" .
|
" 'is_default' => true,\n" .
|
||||||
"]);\n\n" .
|
"]);\n\n" .
|
||||||
"Option 2: Price inheritance from single items\n" .
|
"Option 2: Price inheritance from single items (Recommended)\n" .
|
||||||
"// Set prices on individual items in the pool\n" .
|
"// Set prices on individual items in the pool\n" .
|
||||||
"foreach (\$poolProduct->singleProducts as \$item) {\n" .
|
"foreach (\$poolProduct->singleProducts as \$item) {\n" .
|
||||||
" ProductPrice::create([\n" .
|
" ProductPrice::create([\n" .
|
||||||
|
|
@ -54,7 +54,8 @@ class HasNoPriceException extends NotPurchasable
|
||||||
"\$poolProduct->setPoolPricingStrategy('average'); // or 'lowest' or 'highest'\n\n" .
|
"\$poolProduct->setPoolPricingStrategy('average'); // or 'lowest' or 'highest'\n\n" .
|
||||||
"Current state:\n" .
|
"Current state:\n" .
|
||||||
"- Pool product has no direct price\n" .
|
"- Pool product has no direct price\n" .
|
||||||
"- No single items have prices to inherit from"
|
"- No single items have prices to inherit from\n\n" .
|
||||||
|
"At least one pricing method must be configured before adding to cart."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,15 +186,23 @@ class Cart extends Model
|
||||||
* Set the default date range for the cart.
|
* Set the default date range for the cart.
|
||||||
* Items without specific dates will use these as fallback.
|
* Items without specific dates will use these as fallback.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface $from Start date
|
* @param \DateTimeInterface|string $from Start date (DateTimeInterface or parsable string)
|
||||||
* @param \DateTimeInterface $until End date
|
* @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 InvalidDateRangeException
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setDates(\DateTimeInterface $from, \DateTimeInterface $until, bool $validateAvailability = true): self
|
public function setDates(\DateTimeInterface|string $from, \DateTimeInterface|string $until, bool $validateAvailability = true): self
|
||||||
{
|
{
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($from)) {
|
||||||
|
$from = Carbon::parse($from);
|
||||||
|
}
|
||||||
|
if (is_string($until)) {
|
||||||
|
$until = Carbon::parse($until);
|
||||||
|
}
|
||||||
|
|
||||||
if ($from >= $until) {
|
if ($from >= $until) {
|
||||||
throw new InvalidDateRangeException();
|
throw new InvalidDateRangeException();
|
||||||
}
|
}
|
||||||
|
|
@ -214,14 +222,19 @@ class Cart extends Model
|
||||||
/**
|
/**
|
||||||
* Set the 'from' date for the cart.
|
* Set the 'from' date for the cart.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface $from Start date
|
* @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 InvalidDateRangeException
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setFromDate(\DateTimeInterface $from, bool $validateAvailability = true): self
|
public function setFromDate(\DateTimeInterface|string $from, bool $validateAvailability = true): self
|
||||||
{
|
{
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($from)) {
|
||||||
|
$from = Carbon::parse($from);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->until_date && $from >= $this->until_date) {
|
if ($this->until_date && $from >= $this->until_date) {
|
||||||
throw new InvalidDateRangeException();
|
throw new InvalidDateRangeException();
|
||||||
}
|
}
|
||||||
|
|
@ -238,14 +251,19 @@ class Cart extends Model
|
||||||
/**
|
/**
|
||||||
* Set the 'until' date for the cart.
|
* Set the 'until' date for the cart.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface $until End date
|
* @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 InvalidDateRangeException
|
||||||
* @throws NotEnoughAvailableInTimespanException
|
* @throws NotEnoughAvailableInTimespanException
|
||||||
*/
|
*/
|
||||||
public function setUntilDate(\DateTimeInterface $until, bool $validateAvailability = true): self
|
public function setUntilDate(\DateTimeInterface|string $until, bool $validateAvailability = true): self
|
||||||
{
|
{
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($until)) {
|
||||||
|
$until = Carbon::parse($until);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->from_date && $this->from_date >= $until) {
|
if ($this->from_date && $this->from_date >= $until) {
|
||||||
throw new InvalidDateRangeException();
|
throw new InvalidDateRangeException();
|
||||||
}
|
}
|
||||||
|
|
@ -597,11 +615,11 @@ class Cart extends Model
|
||||||
|
|
||||||
// Ensure prices are not null
|
// Ensure prices are not null
|
||||||
if ($pricePerDay === null) {
|
if ($pricePerDay === null) {
|
||||||
$debugInfo = '';
|
|
||||||
if ($cartable instanceof Product && $cartable->isPool()) {
|
if ($cartable instanceof Product && $cartable->isPool()) {
|
||||||
$debugInfo = " (Pool product, currentQuantityInCart: {$currentQuantityInCart}, hasPrice: " . ($cartable->hasPrice() ? 'yes' : 'no') . ")";
|
// 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.{$debugInfo}");
|
throw new \Exception("Product '{$cartable->name}' has no valid price.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate days if booking dates provided
|
// Calculate days if booking dates provided
|
||||||
|
|
|
||||||
|
|
@ -376,15 +376,22 @@ class CartItem extends Model
|
||||||
* NOTE: This method allows setting any dates, even if they're not available.
|
* NOTE: This method allows setting any dates, even if they're not available.
|
||||||
* Use the is_ready_to_checkout attribute to check if the dates are valid.
|
* Use the is_ready_to_checkout attribute to check if the dates are valid.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface|null $from Start date
|
* @param \DateTimeInterface|string|null $from Start date (DateTimeInterface or parsable string)
|
||||||
* @param \DateTimeInterface|null $until End date
|
* @param \DateTimeInterface|string|null $until End date (DateTimeInterface or parsable string)
|
||||||
* @return $this
|
* @return $this
|
||||||
* @throws \Exception If dates are invalid
|
* @throws \Exception If dates are invalid
|
||||||
*/
|
*/
|
||||||
public function updateDates(
|
public function updateDates(
|
||||||
\DateTimeInterface|null $from = null,
|
\DateTimeInterface|string|null $from = null,
|
||||||
\DateTimeInterface|null $until = null
|
\DateTimeInterface|string|null $until = null
|
||||||
): self {
|
): self {
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($from)) {
|
||||||
|
$from = \Carbon\Carbon::parse($from);
|
||||||
|
}
|
||||||
|
if (is_string($until)) {
|
||||||
|
$until = \Carbon\Carbon::parse($until);
|
||||||
|
}
|
||||||
if ($from >= $until && $until) {
|
if ($from >= $until && $until) {
|
||||||
throw new \Exception("The 'from' date must be before the 'until' date.");
|
throw new \Exception("The 'from' date must be before the 'until' date.");
|
||||||
}
|
}
|
||||||
|
|
@ -421,12 +428,17 @@ class CartItem extends Model
|
||||||
/**
|
/**
|
||||||
* Set the 'from' date for this cart item.
|
* Set the 'from' date for this cart item.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface $from Start date
|
* @param \DateTimeInterface|string $from Start date (DateTimeInterface or parsable string)
|
||||||
* @return $this
|
* @return $this
|
||||||
* @throws InvalidDateRangeException
|
* @throws InvalidDateRangeException
|
||||||
*/
|
*/
|
||||||
public function setFromDate(\DateTimeInterface $from): self
|
public function setFromDate(\DateTimeInterface|string $from): self
|
||||||
{
|
{
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($from)) {
|
||||||
|
$from = \Carbon\Carbon::parse($from);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->until && $from >= $this->until) {
|
if ($this->until && $from >= $this->until) {
|
||||||
throw new InvalidDateRangeException();
|
throw new InvalidDateRangeException();
|
||||||
}
|
}
|
||||||
|
|
@ -448,12 +460,17 @@ class CartItem extends Model
|
||||||
/**
|
/**
|
||||||
* Set the 'until' date for this cart item.
|
* Set the 'until' date for this cart item.
|
||||||
*
|
*
|
||||||
* @param \DateTimeInterface $until End date
|
* @param \DateTimeInterface|string $until End date (DateTimeInterface or parsable string)
|
||||||
* @return $this
|
* @return $this
|
||||||
* @throws InvalidDateRangeException
|
* @throws InvalidDateRangeException
|
||||||
*/
|
*/
|
||||||
public function setUntilDate(\DateTimeInterface $until): self
|
public function setUntilDate(\DateTimeInterface|string $until): self
|
||||||
{
|
{
|
||||||
|
// Parse string dates using Carbon
|
||||||
|
if (is_string($until)) {
|
||||||
|
$until = \Carbon\Carbon::parse($until);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->from && $this->from >= $until) {
|
if ($this->from && $this->from >= $until) {
|
||||||
throw new InvalidDateRangeException();
|
throw new InvalidDateRangeException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -395,10 +395,12 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
|
|
||||||
if ($cart) {
|
if ($cart) {
|
||||||
// Cart-aware: Use smarter pricing that considers which price tiers are used
|
// Cart-aware: Use smarter pricing that considers which price tiers are used
|
||||||
|
// This returns null if no items are available (all sold out)
|
||||||
return $this->getNextAvailablePoolPriceConsideringCart($cart, $sales_price);
|
return $this->getNextAvailablePoolPriceConsideringCart($cart, $sales_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cart and no user: Get inherited price based on strategy (lowest/highest/average of ALL available items)
|
// No cart: Get inherited price from single items
|
||||||
|
// This returns null if no items are available OR if items exist but have no prices
|
||||||
return $this->getInheritedPoolPrice($sales_price);
|
return $this->getInheritedPoolPrice($sales_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,10 +473,13 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($singleItemsWithPrices->isEmpty()) {
|
if ($singleItemsWithPrices->isEmpty()) {
|
||||||
$errors[] = "Pool product has no pricing (direct or inherited)";
|
// Pool has no direct price AND no single items with prices
|
||||||
if ($throwExceptions) {
|
// This is only an error if we're actually trying to use the price
|
||||||
throw HasNoPriceException::poolProductNoPriceAndNoSingleItemPrices($this->name);
|
// So we don't throw here - let the actual usage point handle it
|
||||||
}
|
$warnings[] = "Pool product has no pricing (direct or inherited). Price will be needed when adding to cart.";
|
||||||
|
} else {
|
||||||
|
// Pool has single items with prices - this is valid
|
||||||
|
$warnings[] = "Pool product uses inherited pricing from single items";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,7 +488,7 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
return $this->validateDirectPricing($throwExceptions);
|
return $this->validateDirectPricing($throwExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool with inherited pricing is valid
|
// Pool without direct pricing is valid as long as it has single items with prices
|
||||||
return [
|
return [
|
||||||
'valid' => empty($errors),
|
'valid' => empty($errors),
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,10 @@ trait MayBePoolProduct
|
||||||
$singleItems = $this->singleProducts;
|
$singleItems = $this->singleProducts;
|
||||||
|
|
||||||
if ($singleItems->isEmpty()) {
|
if ($singleItems->isEmpty()) {
|
||||||
|
// No single items, fall back to pool's direct price if available
|
||||||
|
if ($this->hasPrice()) {
|
||||||
|
return $this->defaultPrice()->first()?->getCurrentPrice($sales_price ?? $this->isOnSale());
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,6 +290,24 @@ trait MayBePoolProduct
|
||||||
})->filter()->values();
|
})->filter()->values();
|
||||||
|
|
||||||
if ($prices->isEmpty()) {
|
if ($prices->isEmpty()) {
|
||||||
|
// Single items exist but either:
|
||||||
|
// 1. None are available (sold out) - return null
|
||||||
|
// 2. None have prices configured - fall back to pool's direct price
|
||||||
|
|
||||||
|
// Check if any items are available but just missing prices
|
||||||
|
$hasAvailableItemsWithoutPrices = $singleItems->contains(function ($item) use ($from, $until) {
|
||||||
|
if ($from && $until) {
|
||||||
|
return $item->isAvailableForBooking($from, $until, 1);
|
||||||
|
}
|
||||||
|
return $item->getAvailableStock() > 0 || !$item->manage_stock;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If items are available but have no prices, use pool's direct price as fallback
|
||||||
|
if ($hasAvailableItemsWithoutPrices && $this->hasPrice()) {
|
||||||
|
return $this->defaultPrice()->first()?->getCurrentPrice($sales_price ?? $this->isOnSale());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items are sold out or pool has no fallback price
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\CartItem;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class CartDateStringParsingTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $user;
|
||||||
|
protected $cart;
|
||||||
|
protected $bookingProduct;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = \Workbench\App\Models\User::factory()->create();
|
||||||
|
|
||||||
|
$this->cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->bookingProduct = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Room',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$this->bookingProduct->increaseStock(5);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $this->bookingProduct->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 10000, // $100/day
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_set_dates_accepts_string_dates()
|
||||||
|
{
|
||||||
|
$cart = $this->cart->setDates('2025-12-20', '2025-12-25', false);
|
||||||
|
|
||||||
|
$this->assertNotNull($cart->from_date);
|
||||||
|
$this->assertNotNull($cart->until_date);
|
||||||
|
$this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d'));
|
||||||
|
$this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_set_dates_accepts_datetime_objects()
|
||||||
|
{
|
||||||
|
$from = Carbon::parse('2025-12-20');
|
||||||
|
$until = Carbon::parse('2025-12-25');
|
||||||
|
|
||||||
|
$cart = $this->cart->setDates($from, $until, false);
|
||||||
|
|
||||||
|
$this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d'));
|
||||||
|
$this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_set_from_date_accepts_string()
|
||||||
|
{
|
||||||
|
$cart = $this->cart->setFromDate('2025-12-20', false);
|
||||||
|
|
||||||
|
$this->assertNotNull($cart->from_date);
|
||||||
|
$this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_set_until_date_accepts_string()
|
||||||
|
{
|
||||||
|
$this->cart->update(['from_date' => Carbon::parse('2025-12-20')]);
|
||||||
|
$cart = $this->cart->setUntilDate('2025-12-25', false);
|
||||||
|
|
||||||
|
$this->assertNotNull($cart->until_date);
|
||||||
|
$this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_set_dates_parses_various_string_formats()
|
||||||
|
{
|
||||||
|
// Test different date string formats that Carbon can parse
|
||||||
|
$testCases = [
|
||||||
|
['2025-12-20', '2025-12-25'],
|
||||||
|
['2025/12/20', '2025/12/25'],
|
||||||
|
['20-12-2025', '25-12-2025'],
|
||||||
|
['December 20, 2025', 'December 25, 2025'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testCases as [$from, $until]) {
|
||||||
|
$cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart = $cart->setDates($from, $until, false);
|
||||||
|
|
||||||
|
$this->assertNotNull($cart->from_date, "Failed to parse: $from");
|
||||||
|
$this->assertNotNull($cart->until_date, "Failed to parse: $until");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_item_set_from_date_accepts_string()
|
||||||
|
{
|
||||||
|
$cartItem = $this->cart->addToCart(
|
||||||
|
$this->bookingProduct,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
Carbon::parse('2025-12-20'),
|
||||||
|
Carbon::parse('2025-12-25')
|
||||||
|
);
|
||||||
|
|
||||||
|
$cartItem = $cartItem->setFromDate('2025-12-21');
|
||||||
|
|
||||||
|
$this->assertEquals('2025-12-21', $cartItem->from->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_item_set_until_date_accepts_string()
|
||||||
|
{
|
||||||
|
$cartItem = $this->cart->addToCart(
|
||||||
|
$this->bookingProduct,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
Carbon::parse('2025-12-20'),
|
||||||
|
Carbon::parse('2025-12-25')
|
||||||
|
);
|
||||||
|
|
||||||
|
$cartItem = $cartItem->setUntilDate('2025-12-26');
|
||||||
|
|
||||||
|
$this->assertEquals('2025-12-26', $cartItem->until->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_item_update_dates_accepts_string_dates()
|
||||||
|
{
|
||||||
|
$cartItem = $this->cart->addToCart(
|
||||||
|
$this->bookingProduct,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
Carbon::parse('2025-12-20'),
|
||||||
|
Carbon::parse('2025-12-25')
|
||||||
|
);
|
||||||
|
|
||||||
|
$cartItem = $cartItem->updateDates('2025-12-21', '2025-12-27');
|
||||||
|
|
||||||
|
$this->assertEquals('2025-12-21', $cartItem->from->format('Y-m-d'));
|
||||||
|
$this->assertEquals('2025-12-27', $cartItem->until->format('Y-m-d'));
|
||||||
|
|
||||||
|
// Verify price was recalculated for new date range (6 days instead of 5)
|
||||||
|
$expectedPrice = 10000 * 6; // $100/day * 6 days
|
||||||
|
$this->assertEquals($expectedPrice, $cartItem->price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_item_update_dates_accepts_mixed_string_and_datetime()
|
||||||
|
{
|
||||||
|
$cartItem = $this->cart->addToCart(
|
||||||
|
$this->bookingProduct,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
Carbon::parse('2025-12-20'),
|
||||||
|
Carbon::parse('2025-12-25')
|
||||||
|
);
|
||||||
|
|
||||||
|
// String from, DateTime until
|
||||||
|
$cartItem = $cartItem->updateDates('2025-12-21', Carbon::parse('2025-12-27'));
|
||||||
|
|
||||||
|
$this->assertEquals('2025-12-21', $cartItem->from->format('Y-m-d'));
|
||||||
|
$this->assertEquals('2025-12-27', $cartItem->until->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_item_date_parsing_works_with_now_relative_strings()
|
||||||
|
{
|
||||||
|
$cartItem = $this->cart->addToCart(
|
||||||
|
$this->bookingProduct,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
Carbon::parse('2025-12-20'),
|
||||||
|
Carbon::parse('2025-12-25')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test relative date strings
|
||||||
|
$cartItem = $cartItem->updateDates('now', '+5 days');
|
||||||
|
|
||||||
|
$this->assertNotNull($cartItem->from);
|
||||||
|
$this->assertNotNull($cartItem->until);
|
||||||
|
$this->assertTrue($cartItem->from < $cartItem->until);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,336 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
|
use Blax\Shop\Exceptions\HasNoPriceException;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class PoolProductPricingFlexibilityTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->user = \Workbench\App\Models\User::factory()->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_without_direct_price_uses_single_item_prices()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 5000, // $50
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// Pool should be able to use single item price
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(5000, $price);
|
||||||
|
|
||||||
|
// Should be able to add to cart without pool having direct price
|
||||||
|
$cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart($pool, 1);
|
||||||
|
$this->assertNotNull($cartItem);
|
||||||
|
$this->assertEquals(5000, $cartItem->price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_validation_does_not_throw_when_single_items_have_prices()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 5000,
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// validatePricing should not throw when single items have prices
|
||||||
|
$result = $pool->validatePricing(throwExceptions: false);
|
||||||
|
|
||||||
|
$this->assertTrue($result['valid']);
|
||||||
|
$this->assertEmpty($result['errors']);
|
||||||
|
$this->assertNotEmpty($result['warnings']);
|
||||||
|
$this->assertStringContainsString('inherited pricing', $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_validation_warns_when_no_prices_available_but_does_not_throw()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Pool Without Any Prices',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// validatePricing should not throw, just return warnings
|
||||||
|
$result = $pool->validatePricing(throwExceptions: false);
|
||||||
|
|
||||||
|
$this->assertTrue($result['valid']); // Changed: should still be valid
|
||||||
|
$this->assertEmpty($result['errors']); // Changed: no errors
|
||||||
|
$this->assertNotEmpty($result['warnings']);
|
||||||
|
$this->assertStringContainsString('Price will be needed when adding to cart', $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_throws_exception_only_when_adding_to_cart_without_any_prices()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Pool Without Any Prices',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
$cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Exception should only be thrown when trying to add to cart
|
||||||
|
$this->expectException(HasNoPriceException::class);
|
||||||
|
$this->expectExceptionMessage('Cannot add pool product');
|
||||||
|
$this->expectExceptionMessage('No pricing available');
|
||||||
|
|
||||||
|
$cart->addToCart($pool, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_with_direct_price_used_as_fallback_when_single_items_have_no_prices()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
// Single item has NO price
|
||||||
|
// Pool has direct price as fallback
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pool->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 4000, // $40 - fallback pool price
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// Pool should use its own direct price as fallback when single items have no prices
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(4000, $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_prefers_single_item_prices_over_direct_price()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
// Single item has price
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 5000, // $50
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Pool also has direct price, but single item price should be preferred
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pool->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 4000, // $40 - pool price (should be ignored)
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// Pool should prefer single item price over its own direct price
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(5000, $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_can_be_created_without_price_if_single_items_will_have_prices()
|
||||||
|
{
|
||||||
|
// This test verifies that pools can exist in a "not fully configured" state
|
||||||
|
// as long as they get prices before being added to cart
|
||||||
|
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Future Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id]);
|
||||||
|
|
||||||
|
// At this point, neither pool nor single items have prices
|
||||||
|
// This should be allowed - pool can exist without prices
|
||||||
|
|
||||||
|
$this->assertNotNull($pool);
|
||||||
|
$this->assertCount(1, $pool->singleProducts);
|
||||||
|
|
||||||
|
// Now add price to single item
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 5000,
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Now pool should be ready to use
|
||||||
|
$cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart($pool, 1);
|
||||||
|
$this->assertNotNull($cartItem);
|
||||||
|
$this->assertEquals(5000, $cartItem->price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function pool_uses_pricing_strategy_with_multiple_single_item_prices()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
$spot2 = Product::factory()->create([
|
||||||
|
'name' => 'Spot 2',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot2->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 3000, // $30
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot2->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 7000, // $70
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$spot1->id, $spot2->id]);
|
||||||
|
|
||||||
|
// By default, should use LOWEST pricing strategy
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(3000, $price);
|
||||||
|
|
||||||
|
// Change to HIGHEST
|
||||||
|
$pool->setPoolPricingStrategy('highest');
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(7000, $price);
|
||||||
|
|
||||||
|
// Change to AVERAGE
|
||||||
|
$pool->setPoolPricingStrategy('average');
|
||||||
|
$price = $pool->getCurrentPrice();
|
||||||
|
$this->assertEquals(5000, $price); // (3000 + 7000) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -207,8 +207,8 @@ class ProductPricingValidationTest extends TestCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->expectException(HasNoPriceException::class);
|
$this->expectException(HasNoPriceException::class);
|
||||||
$this->expectExceptionMessage('Pool product');
|
$this->expectExceptionMessage('Cannot add pool product');
|
||||||
$this->expectExceptionMessage('has no pricing configured');
|
$this->expectExceptionMessage('No pricing available');
|
||||||
|
|
||||||
Cart::add($pool, 1);
|
Cart::add($pool, 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue